diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java index 8ee51e993..ab37c0ac9 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/api/AuthApiController.java @@ -24,7 +24,6 @@ import site.timecapsulearchive.core.domain.auth.data.response.VerificationMessageSendResponse; import site.timecapsulearchive.core.domain.auth.service.MessageVerificationService; import site.timecapsulearchive.core.domain.auth.service.TokenManager; -import site.timecapsulearchive.core.domain.member.data.mapper.MemberMapper; import site.timecapsulearchive.core.domain.member.service.MemberService; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.common.response.SuccessCode; @@ -40,7 +39,6 @@ public class AuthApiController implements AuthApi { private final TokenManager tokenService; private final MessageVerificationService messageVerificationService; private final MemberService memberService; - private final MemberMapper memberMapper; @GetMapping(value = "/login/url/kakao", produces = {"application/json"}) @@ -126,7 +124,7 @@ public ResponseEntity> reIssueAccessToken( public ResponseEntity> signUpWithSocialProvider( @Valid @RequestBody final SignUpRequest request ) { - final Long id = memberService.createMember(memberMapper.signUpRequestToDto(request)); + final Long id = memberService.createMember(request.toDto()); return ResponseEntity.ok( ApiSpec.success( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/data/request/SignUpRequest.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/data/request/SignUpRequest.java index 9ee31d4f1..01b421111 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/data/request/SignUpRequest.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/data/request/SignUpRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import site.timecapsulearchive.core.domain.member.data.dto.SignUpRequestDto; import site.timecapsulearchive.core.domain.member.entity.SocialType; @Schema(description = "소셜 프로바이더의 인증 아이디로 회원가입 요청") @@ -27,4 +28,13 @@ public record SignUpRequest( SocialType socialType ) { + public SignUpRequestDto toDto() { + return new SignUpRequestDto( + authId, + email, + profileUrl, + socialType + ); + } + } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java index 7d641553e..69cd6f204 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/auth/service/MessageVerificationService.java @@ -11,8 +11,10 @@ import site.timecapsulearchive.core.domain.auth.exception.CertificationNumberNotMatchException; import site.timecapsulearchive.core.domain.auth.repository.MessageAuthenticationCacheRepository; import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.domain.member.entity.MemberTemporary; import site.timecapsulearchive.core.domain.member.exception.MemberNotFoundException; import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.domain.member.repository.MemberTemporaryRepository; import site.timecapsulearchive.core.global.security.encryption.AESEncryptionManager; import site.timecapsulearchive.core.global.security.encryption.HashEncryptionManager; import site.timecapsulearchive.core.infra.sms.data.response.SmsApiResponse; @@ -30,6 +32,7 @@ public class MessageVerificationService { private final MessageAuthenticationCacheRepository messageAuthenticationCacheRepository; private final SmsApiManager smsApiManager; private final MemberRepository memberRepository; + private final MemberTemporaryRepository memberTemporaryRepository; private final TokenManager tokenManager; private final AESEncryptionManager aesEncryptionManager; @@ -90,9 +93,9 @@ public TokenResponse validVerificationMessage( throw new CertificationNumberNotMatchException(); } - updateMemberData(memberId, plain); + final Long verifiedMemberId = updateToVerifiedMember(memberId, plain); - return tokenManager.createNewToken(memberId); + return tokenManager.createNewToken(verifiedMemberId); } private boolean isNotMatch(final String certificationNumber, @@ -100,12 +103,17 @@ private boolean isNotMatch(final String certificationNumber, return !certificationNumber.equals(findCertificationNumber); } - private void updateMemberData(final Long memberId, final byte[] plain) { - final Member findMember = memberRepository.findMemberById(memberId) + private Long updateToVerifiedMember(final Long memberId, final byte[] plain) { + final MemberTemporary memberTemporary = memberTemporaryRepository.findById(memberId) .orElseThrow(MemberNotFoundException::new); - findMember.updatePhoneHash(hashEncryptionManager.encrypt(plain)); - findMember.updatePhoneNumber(aesEncryptionManager.encryptWithPrefixIV(plain)); - findMember.updateVerification(); + memberTemporaryRepository.delete(memberTemporary); + + final Member verifiedMember = memberTemporary.toMember(hashEncryptionManager.encrypt(plain), + aesEncryptionManager.encryptWithPrefixIV(plain)); + + memberRepository.save(verifiedMember); + + return verifiedMember.getId(); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/entity/GroupCapsuleOpen.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/entity/GroupCapsuleOpen.java index 45ef508db..b2c08a19e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/entity/GroupCapsuleOpen.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/entity/GroupCapsuleOpen.java @@ -9,7 +9,9 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import java.util.Objects; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -26,6 +28,9 @@ public class GroupCapsuleOpen extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "is_opened", nullable = false) + private Boolean isOpened; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "capsule_id", nullable = false) private Capsule capsule; @@ -33,4 +38,11 @@ public class GroupCapsuleOpen extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) private Member member; + + @Builder + private GroupCapsuleOpen(Boolean isOpened, Capsule capsule, Member member) { + this.isOpened = Objects.requireNonNull(isOpened); + this.capsule = Objects.requireNonNull(capsule); + this.member = Objects.requireNonNull(member); + } } 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..f8ea94237 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 @@ -8,9 +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 org.hibernate.validator.constraints.Range; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; 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; @@ -18,6 +19,7 @@ import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.error.ErrorResponse; +@Validated public interface CapsuleApi { @Operation( @@ -56,14 +58,16 @@ ResponseEntity getImages( description = "잘못된 요청 파라미터에 의해 발생" ) }) - ResponseEntity> getNearbyARCapsules( + ResponseEntity> getARNearbyMyCapsules( Long memberId, @Parameter(in = ParameterIn.QUERY, description = "위도(wsg84)", required = true) - double longitude, + @Range(min = -90, max = 90, message = "위도는 -90과 90 사이여야 합니다.") + double latitude, @Parameter(in = ParameterIn.QUERY, description = "경도(wsg84)", required = true) - double latitude, + @Range(min = -180, max = 180, message = "경도는 -180과 180 사이여야 합니다.") + double longitude, @Parameter(in = ParameterIn.QUERY, description = "조회 거리(km)", required = true) double distance, @@ -88,14 +92,16 @@ ResponseEntity> getNearbyARCapsules( description = "잘못된 요청 파라미터에 의해 발생" ) }) - ResponseEntity> getNearbyCapsules( + ResponseEntity> getMapNearbyMyCapsules( Long memberId, @Parameter(in = ParameterIn.QUERY, description = "위도(wsg84)", required = true) - double longitude, + @Range(min = -90, max = 90, message = "위도는 -90과 90 사이여야 합니다.") + double latitude, @Parameter(in = ParameterIn.QUERY, description = "경도(wsg84)", required = true) - double latitude, + @Range(min = -180, max = 180, message = "경도는 -180과 180 사이여야 합니다.") + double longitude, @Parameter(in = ParameterIn.QUERY, description = "조회 거리(km)", required = true) double distance, @@ -105,8 +111,8 @@ ResponseEntity> getNearbyCapsules( ); @Operation( - summary = "캡슐 열람 상태로 변경", - description = "캡슐을 열람 상태를 변경해준다.", + summary = "현재 사용자 위치 기준으로 사용자의 친구들의 지도용 캡슐 목록 조회", + description = "지도에서 캡슐을 보기 위해 현재 사용자 위치를 바탕으로 반경 distance km만큼 친구들의 캡슐을 조회한다.", security = {@SecurityRequirement(name = "user_token")}, tags = {"capsule"} ) @@ -116,70 +122,78 @@ ResponseEntity> getNearbyCapsules( description = "처리 완료" ), @ApiResponse( - responseCode = "404", - description = "해당 캡슐을 찾을 수 없을 경우 발생하는 예외", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + responseCode = "400", + description = "잘못된 요청 파라미터에 의해 발생" ) }) - ResponseEntity> updateCapsuleOpened( + ResponseEntity> getMapNearbyFriendsCapsules( Long memberId, - @Parameter(in = ParameterIn.PATH, description = "캡슐 아이디", required = true) - Long capsuleId + @Parameter(in = ParameterIn.QUERY, description = "위도(wsg84)", required = true) + @Range(min = -90, max = 90, message = "위도는 -90과 90 사이여야 합니다.") + double latitude, + + @Parameter(in = ParameterIn.QUERY, description = "경도(wsg84)", required = true) + @Range(min = -180, max = 180, message = "경도는 -180과 180 사이여야 합니다.") + double longitude, + + @Parameter(in = ParameterIn.QUERY, description = "조회 거리(km)", required = true) + double distance ); @Operation( - summary = "비밀 캡슐 생성", - description = "사용자만 볼 수 있는 비밀 캡슐을 생성한다.", + summary = "현재 사용자 위치 기준으로 사용자의 친구들의 AR용 캡슐 목록 조회", + description = "AR 카메라로 캡슐을 보기 위해 현재 사용자 위치를 바탕으로 반경 distance km만큼 친구들의 캡슐을 조회한다.", security = {@SecurityRequirement(name = "user_token")}, - tags = {"secret capsule"} + tags = {"capsule"} ) @ApiResponses(value = { @ApiResponse( - responseCode = "202", - description = "처리 시작" + responseCode = "200", + description = "처리 완료" ), @ApiResponse( responseCode = "400", - description = "좌표변환을 할 수 없을 때 발생하는 예외, 입력좌표 확인 요망", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) - ), - @ApiResponse( - responseCode = "404", - description = "캡슐 스킨을 찾을 수 없을 경우 발생하는 예외", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + description = "잘못된 요청 파라미터에 의해 발생" ) }) - ResponseEntity> createSecretCapsule( + ResponseEntity> getARNearbyFriendsCapsules( Long memberId, - CapsuleCreateRequest request + + @Parameter(in = ParameterIn.QUERY, description = "위도(wsg84)", required = true) + @Range(min = -90, max = 90, message = "위도는 -90과 90 사이여야 합니다.") + double latitude, + + @Parameter(in = ParameterIn.QUERY, description = "경도(wsg84)", required = true) + @Range(min = -180, max = 180, message = "경도는 -180과 180 사이여야 합니다.") + double longitude, + + @Parameter(in = ParameterIn.QUERY, description = "조회 거리(km)", required = true) + double distance ); @Operation( - summary = "공개 캡슐 생성", - description = "사용자의 친구들만 볼 수 있는 공개 캡슐을 생성한다.", + summary = "캡슐 열람 상태로 변경", + description = "캡슐을 열람 상태를 변경해준다.", security = {@SecurityRequirement(name = "user_token")}, - tags = {"public capsule"} + tags = {"capsule"} ) @ApiResponses(value = { @ApiResponse( - responseCode = "202", - description = "처리 시작" - ), - @ApiResponse( - responseCode = "400", - description = "좌표변환을 할 수 없을 때 발생하는 예외, 입력좌표 확인 요망", - content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + responseCode = "200", + description = "처리 완료" ), @ApiResponse( responseCode = "404", - description = "캡슐 스킨을 찾을 수 없을 경우 발생하는 예외", + description = "해당 캡슐을 찾을 수 없을 경우 발생하는 예외", content = @Content(schema = @Schema(implementation = ErrorResponse.class)) ) }) - ResponseEntity> createPublicCapsule( + ResponseEntity> updateCapsuleOpened( Long memberId, - CapsuleCreateRequest request + + @Parameter(in = ParameterIn.PATH, description = "캡슐 아이디", required = true) + Long capsuleId ); } 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..eef579899 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; @@ -45,9 +41,9 @@ public ResponseEntity getImages(final Long size, final Long return null; } - @GetMapping(value = "/nearby/ar", produces = {"application/json"}) + @GetMapping(value = "/my/ar/nearby", produces = {"application/json"}) @Override - public ResponseEntity> getNearbyARCapsules( + public ResponseEntity> getARNearbyMyCapsules( @AuthenticationPrincipal final Long memberId, @RequestParam(value = "latitude") final double latitude, @RequestParam(value = "longitude") final double longitude, @@ -72,9 +68,9 @@ public ResponseEntity> getNearbyARCapsules( ); } - @GetMapping(value = "/nearby", produces = {"application/json"}) + @GetMapping(value = "/my/map/nearby", produces = {"application/json"}) @Override - public ResponseEntity> getNearbyCapsules( + public ResponseEntity> getMapNearbyMyCapsules( @AuthenticationPrincipal final Long memberId, @RequestParam(value = "latitude") final double latitude, @RequestParam(value = "longitude") final double longitude, @@ -95,46 +91,64 @@ public ResponseEntity> getNearbyCapsules( ); } - @PatchMapping(value = "/{capsule_id}/opened", produces = {"application/json"}) + @GetMapping(value = "/friends/map/nearby", produces = {"application/json"}) @Override - public ResponseEntity> updateCapsuleOpened( + public ResponseEntity> getMapNearbyFriendsCapsules( @AuthenticationPrincipal final Long memberId, - @PathVariable("capsule_id") final Long capsuleId + @RequestParam(value = "latitude") final double latitude, + @RequestParam(value = "longitude") final double longitude, + @RequestParam(value = "distance") final double distance ) { + final List capsules = capsuleService.findFriendsCapsulesByCurrentLocation( + memberId, + CoordinateRangeDto.from(latitude, longitude, distance) + ); + return ResponseEntity.ok( ApiSpec.success( SuccessCode.SUCCESS, - capsuleFacade.updateCapsuleOpened(memberId, capsuleId) + NearbyCapsuleResponse.createOf(capsules, geoTransformManager::changePoint3857To4326) ) ); } - @PostMapping(value = "/secret", consumes = {"application/json"}) + @GetMapping(value = "/friends/ar/nearby", produces = {"application/json"}) @Override - public ResponseEntity> createSecretCapsule( + public ResponseEntity> getARNearbyFriendsCapsules( @AuthenticationPrincipal final Long memberId, - @Valid @RequestBody final CapsuleCreateRequest request + @RequestParam(value = "latitude") final double latitude, + @RequestParam(value = "longitude") final double longitude, + @RequestParam(value = "distance") final double distance ) { - capsuleFacade.saveCapsule(memberId, request.toDto(), CapsuleType.SECRET); + final List capsules = capsuleService.findFriendsARCapsulesByCurrentLocation( + memberId, + CoordinateRangeDto.from(latitude, longitude, distance) + ); return ResponseEntity.ok( - ApiSpec.empty( - SuccessCode.SUCCESS + ApiSpec.success( + SuccessCode.SUCCESS, + NearbyARCapsuleResponse.createOf( + capsules, + geoTransformManager::changePoint3857To4326, + s3PreSignedUrlManager::getS3PreSignedUrlForGet + ) ) ); } - @PostMapping(value = "/public", consumes = {"application/json"}) + @PatchMapping(value = "/{capsule_id}/opened", produces = {"application/json"}) @Override - public ResponseEntity> createPublicCapsule( + public ResponseEntity> updateCapsuleOpened( @AuthenticationPrincipal final Long memberId, - @Valid @RequestBody final CapsuleCreateRequest request) { - capsuleFacade.saveCapsule(memberId, request.toDto(), CapsuleType.PUBLIC); - + @PathVariable("capsule_id") final Long capsuleId + ) { return ResponseEntity.ok( - ApiSpec.empty( - SuccessCode.SUCCESS + ApiSpec.success( + SuccessCode.SUCCESS, + capsuleFacade.updateCapsuleOpened(memberId, capsuleId) ) ); } + } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleDetailDto.java index 32d1c35ad..da966b437 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleDetailDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleDetailDto.java @@ -1,7 +1,11 @@ package site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto; import java.time.ZonedDateTime; +import java.util.List; +import java.util.function.Function; +import org.locationtech.jts.geom.Point; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleDetailResponse; public record CapsuleDetailDto( Long capsuleId, @@ -10,6 +14,7 @@ public record CapsuleDetailDto( String nickname, String profileUrl, ZonedDateTime createdAt, + Point point, String address, String roadName, String title, @@ -28,6 +33,7 @@ public CapsuleDetailDto excludeTitleAndContentAndImagesAndVideos() { nickname, profileUrl, createdAt, + point, address, roadName, "", @@ -38,4 +44,35 @@ public CapsuleDetailDto excludeTitleAndContentAndImagesAndVideos() { capsuleType ); } + + public CapsuleDetailResponse toResponse( + final Function singlePreSignUrlFunction, + final Function> multiplePreSignUrlFunction, + final Function changePointFunction + ) { + final List preSignedImageUrls = multiplePreSignUrlFunction.apply( + images); + final List preSignedVideoUrls = multiplePreSignUrlFunction.apply( + videos); + + final Point changePoint = changePointFunction.apply(point); + + return CapsuleDetailResponse.builder() + .capsuleSkinUrl(singlePreSignUrlFunction.apply(capsuleSkinUrl)) + .dueDate(dueDate) + .nickname(nickname) + .profileUrl(profileUrl) + .createdDate(createdAt) + .latitude(changePoint.getX()) + .longitude(changePoint.getY()) + .address(address) + .roadName(roadName) + .title(title) + .content(content) + .imageUrls(preSignedImageUrls) + .videoUrls(preSignedVideoUrls) + .isOpened(isOpened) + .capsuleType(capsuleType) + .build(); + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleSummaryDto.java index 517b9096e..621b95d0e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleSummaryDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/dto/CapsuleSummaryDto.java @@ -1,6 +1,9 @@ package site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto; import java.time.ZonedDateTime; +import java.util.function.Function; +import org.locationtech.jts.geom.Point; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.response.CapsuleSummaryResponse; public record CapsuleSummaryDto( String nickname, @@ -8,10 +11,32 @@ public record CapsuleSummaryDto( String skinUrl, String title, ZonedDateTime dueDate, + Point point, String address, String roadName, Boolean isOpened, ZonedDateTime createdAt ) { + public CapsuleSummaryResponse toResponse( + final Function preSignUrlFunction, + final Function changePointFunction + ) { + final Point changePoint = changePointFunction.apply(point); + + return CapsuleSummaryResponse.builder() + .nickname(nickname) + .profileUrl(profileUrl) + .skinUrl(preSignUrlFunction.apply(skinUrl)) + .title(title) + .dueDate(dueDate) + .latitude(changePoint.getX()) + .longitude(changePoint.getY()) + .address(address) + .roadName(roadName) + .isOpened(isOpened) + .createdAt(createdAt) + .build(); + } + } \ No newline at end of file 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/CapsuleDetailResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleDetailResponse.java index d6e835e3c..f94246978 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleDetailResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleDetailResponse.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.function.Function; import lombok.Builder; +import org.locationtech.jts.geom.Point; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @@ -28,6 +29,12 @@ public record CapsuleDetailResponse( @Schema(description = "생성일") ZonedDateTime createdDate, + @Schema(description = "캡슐 위도 좌표") + Double latitude, + + @Schema(description = "캡슐 경도 좌표") + Double longitude, + @Schema(description = "캡슐 생성 주소") String address, @@ -58,33 +65,18 @@ public record CapsuleDetailResponse( dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } - createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + if (createdDate != null) { + createdDate = createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } } public static CapsuleDetailResponse createOf( final CapsuleDetailDto detailDto, final Function singlePreSignUrlFunction, - final Function> multiplePreSignUrlFunction + final Function> multiplePreSignUrlFunction, + final Function changePointFunction ) { - final List preSignedImageUrls = multiplePreSignUrlFunction.apply( - detailDto.images()); - final List preSignedVideoUrls = multiplePreSignUrlFunction.apply( - detailDto.videos()); - - return new CapsuleDetailResponse( - singlePreSignUrlFunction.apply(detailDto.capsuleSkinUrl()), - detailDto.dueDate(), - detailDto.nickname(), - detailDto.profileUrl(), - detailDto.createdAt(), - detailDto.address(), - detailDto.roadName(), - detailDto.title(), - detailDto.content(), - preSignedImageUrls, - preSignedVideoUrls, - detailDto.isOpened(), - detailDto.capsuleType() - ); + return detailDto.toResponse(singlePreSignUrlFunction, multiplePreSignUrlFunction, + changePointFunction); } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java index 4291abf49..b96f6b99e 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/CapsuleSummaryResponse.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import java.util.function.Function; import lombok.Builder; +import org.locationtech.jts.geom.Point; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @@ -26,6 +27,12 @@ public record CapsuleSummaryResponse( @Schema(description = "개봉일") ZonedDateTime dueDate, + @Schema(description = "캡슐 위도 좌표") + Double latitude, + + @Schema(description = "캡슐 경도 좌표") + Double longitude, + @Schema(description = "캡슐 생성 주소") String address, @@ -44,23 +51,16 @@ public record CapsuleSummaryResponse( dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } - createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } } public static CapsuleSummaryResponse createOf( final CapsuleSummaryDto summaryDto, - final Function preSignUrlFunction + final Function preSignUrlFunction, + final Function changePointFunction ) { - return new CapsuleSummaryResponse( - summaryDto.nickname(), - summaryDto.profileUrl(), - preSignUrlFunction.apply(summaryDto.skinUrl()), - summaryDto.title(), - summaryDto.dueDate(), - summaryDto.address(), - summaryDto.roadName(), - summaryDto.isOpened(), - summaryDto.createdAt() - ); + return summaryDto.toResponse(preSignUrlFunction, changePointFunction); } } 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..66eaa0398 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/data/response/NearbyARCapsuleSummaryResponse.java @@ -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, @@ -33,5 +34,9 @@ public record NearbyARCapsuleSummaryResponse( @Schema(description = "캡슐 타입") 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..505e9371e 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,115 +22,144 @@ @RequiredArgsConstructor public class CapsuleQueryRepository { - private final EntityManager entityManager; + private final JPAQueryFactory jpaQueryFactory; /** - * 캡슐 타입에 따라 현재 위치에서 범위 내의 캡슐을 조회한다. + * AR에서 캡슐을 찾기 위해 캡슐 타입에 따라 현재 위치에서 범위 내의 사용자가 만든 캡슐을 조회한다. * * @param memberId 범위 내의 캡슙을 조회할 멤버 id - * @param mbr 캡슈을 조회할 범위 + * @param mbr 캡슐을 조회할 범위(최소사각형) * @param capsuleType 조회할 캡슐의 타입 * @return 범위 내에 조회된 캡슐들의 요약 정보들을 반환한다. + * @see site.timecapsulearchive.core.global.geography.GeoTransformManager */ public List findARCapsuleSummaryDtosByCurrentLocationAndCapsuleType( final Long memberId, 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)))) + .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..f1414845b 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,53 @@ 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 6bbefaf11..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,12 +62,12 @@ ResponseEntity> createGroupCapsule( @Parameter(in = ParameterIn.PATH, description = "생성할 그룹 아이디", required = true) Long groupId, - GroupCapsuleCreateRequest request + @Valid GroupCapsuleCreateRequest request ); @Operation( summary = "그룹 캡슐 상세 조회", - description = "그룹원만 볼 수 있는 그룹 캡슐 내용을 조회한다.", + description = "그룹원만 볼 수 있는 그룹 캡슐 내용을 상세 조회한다.", security = {@SecurityRequirement(name = "user_token")}, tags = {"group capsule"} ) @@ -72,16 +77,30 @@ ResponseEntity> createGroupCapsule( description = "처리 완료" ) }) - @GetMapping( - value = "/groups/{group_id}/capsules/{capsule_id}", - produces = {"application/json"} + ResponseEntity> getGroupCapsuleDetailByCapsuleId( + Long memberId, + + @Parameter(in = ParameterIn.PATH, description = "조회할 캡슐 아이디", required = true) + Long capsuleId + ); + + @Operation( + summary = "그룹 캡슐 요약 조회", + description = "그룹원만 볼 수 있는 그룹 캡슐 내용을 요약 조회한다.", + security = {@SecurityRequirement(name = "user_token")}, + tags = {"group capsule"} ) - ResponseEntity findGroupCapsuleByIdAndGroupId( - @Parameter(in = ParameterIn.PATH, description = "조회할 그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId, + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "처리 완료" + ) + }) + ResponseEntity> getGroupCapsuleSummaryByCapsuleId( + Long memberId, - @Parameter(in = ParameterIn.PATH, description = "조회할 캡슐 아이디", required = true, schema = @Schema()) - @PathVariable("capsule_id") Long capsuleId + @Parameter(in = ParameterIn.PATH, description = "조회할 캡슐 아이디", required = true) + Long capsuleId ); @Operation( @@ -111,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 80736119b..c7e7f6af1 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,32 +1,46 @@ 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; 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.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.GroupCapsuleSummaryResponse; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.MyGroupCapsuleSliceResponse; import site.timecapsulearchive.core.domain.capsule.group_capsule.facade.GroupCapsuleFacade; +import site.timecapsulearchive.core.domain.capsule.group_capsule.service.GroupCapsuleService; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.common.response.SuccessCode; +import site.timecapsulearchive.core.global.geography.GeoTransformManager; +import site.timecapsulearchive.core.infra.s3.manager.S3PreSignedUrlManager; @RestController -@RequestMapping("/group") +@RequestMapping("/groups-capsules") @RequiredArgsConstructor public class GroupCapsuleApiController implements GroupCapsuleApi { private final GroupCapsuleFacade groupCapsuleFacade; + private final GroupCapsuleService groupCapsuleService; + private final S3PreSignedUrlManager s3PreSignedUrlManager; + private final GeoTransformManager geoTransformManager; @PostMapping( - value = "/{group_id}/capsules", + value = "/{group_id}", consumes = {"application/json"} ) @Override @@ -43,10 +57,53 @@ public ResponseEntity> createGroupCapsule( ); } + @GetMapping( + value = "/{capsule_id}/detail", + produces = {"application/json"} + ) @Override - public ResponseEntity findGroupCapsuleByIdAndGroupId(Long groupId, - Long capsuleId) { - return null; + public ResponseEntity> getGroupCapsuleDetailByCapsuleId( + @AuthenticationPrincipal Long memberId, + @PathVariable("capsule_id") Long capsuleId + ) { + final GroupCapsuleDetailDto detailDto = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( + capsuleId); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + GroupCapsuleDetailResponse.createOf( + detailDto, + s3PreSignedUrlManager::getS3PreSignedUrlForGet, + s3PreSignedUrlManager::getS3PreSignedUrlsForGet, + geoTransformManager::changePoint3857To4326 + ) + ) + ); + } + + @GetMapping( + value = "/{capsule_id}/summary", + produces = {"application/json"} + ) + @Override + public ResponseEntity> getGroupCapsuleSummaryByCapsuleId( + @AuthenticationPrincipal Long memberId, + @PathVariable("capsule_id") Long capsuleId + ) { + final GroupCapsuleSummaryDto summaryDto = groupCapsuleService.findGroupCapsuleSummaryByGroupIDAndCapsuleId( + capsuleId); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + GroupCapsuleSummaryResponse.createOf( + summaryDto, + s3PreSignedUrlManager::getS3PreSignedUrlForGet, + geoTransformManager::changePoint3857To4326 + ) + ) + ); } @Override @@ -55,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/GroupCapsuleCreateRequestDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleCreateRequestDto.java index a98e16fdb..603fd70bf 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleCreateRequestDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleCreateRequestDto.java @@ -13,6 +13,7 @@ @Builder public record GroupCapsuleCreateRequestDto( + List groupMemberIds, List imageNames, List videoNames, Long capsuleSkinId, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleDetailDto.java new file mode 100644 index 000000000..e47993f89 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleDetailDto.java @@ -0,0 +1,61 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto; + +import java.util.List; +import java.util.function.Function; +import org.locationtech.jts.geom.Point; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsuleDetailResponse; +import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberSummaryDto; +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; + +public record GroupCapsuleDetailDto( + CapsuleDetailDto capsuleDetailDto, + List members +) { + + public List groupMemberSummaryDtoToResponse() { + return members.stream() + .map(GroupMemberSummaryDto::toResponse) + .toList(); + } + + public GroupCapsuleDetailDto excludeDetailContents() { + return new GroupCapsuleDetailDto( + capsuleDetailDto.excludeTitleAndContentAndImagesAndVideos(), + members + ); + } + + public GroupCapsuleDetailResponse toResponse( + final Function singlePreSignUrlFunction, + final Function> multiplePreSignUrlFunction, + final Function changePointFunction + ) { + final Point changePoint = changePointFunction.apply(capsuleDetailDto.point()); + + final List preSignedImageUrls = multiplePreSignUrlFunction.apply( + capsuleDetailDto.images()); + final List preSignedVideoUrls = multiplePreSignUrlFunction.apply( + capsuleDetailDto.videos()); + + return GroupCapsuleDetailResponse.builder() + .capsuleId(capsuleDetailDto.capsuleId()) + .capsuleSkinUrl(singlePreSignUrlFunction.apply(capsuleDetailDto.capsuleSkinUrl())) + .members(groupMemberSummaryDtoToResponse()) + .dueDate(capsuleDetailDto.dueDate()) + .nickname(capsuleDetailDto.nickname()) + .profileUrl(capsuleDetailDto.profileUrl()) + .createdDate(capsuleDetailDto.createdAt()) + .latitude(changePoint.getX()) + .longitude(changePoint.getY()) + .address(capsuleDetailDto.address()) + .roadName(capsuleDetailDto().roadName()) + .title(capsuleDetailDto().title()) + .content(capsuleDetailDto.content()) + .imageUrls(preSignedImageUrls) + .videoUrls(preSignedVideoUrls) + .isOpened(capsuleDetailDto.isOpened()) + .capsuleType(capsuleDetailDto.capsuleType()) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleSummaryDto.java new file mode 100644 index 000000000..b3306cf0a --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/dto/GroupCapsuleSummaryDto.java @@ -0,0 +1,43 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto; + +import java.util.List; +import java.util.function.Function; +import org.locationtech.jts.geom.Point; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleSummaryDto; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.response.GroupCapsuleSummaryResponse; +import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberSummaryDto; +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; + +public record GroupCapsuleSummaryDto( + CapsuleSummaryDto capsuleSummaryDto, + List members +) { + + public List groupMemberSummaryDtoToResponse() { + return members.stream() + .map(GroupMemberSummaryDto::toResponse) + .toList(); + } + + public GroupCapsuleSummaryResponse toResponse( + final Function preSignUrlFunction, + final Function changePointFunction + ) { + final Point changePoint = changePointFunction.apply(capsuleSummaryDto.point()); + + return GroupCapsuleSummaryResponse.builder() + .members(groupMemberSummaryDtoToResponse()) + .nickname(capsuleSummaryDto.nickname()) + .profileUrl(capsuleSummaryDto.profileUrl()) + .skinUrl(preSignUrlFunction.apply(capsuleSummaryDto.skinUrl())) + .title(capsuleSummaryDto.title()) + .dueDate(capsuleSummaryDto.dueDate()) + .latitude(changePoint.getX()) + .longitude(changePoint.getY()) + .address(capsuleSummaryDto.address()) + .roadName(capsuleSummaryDto.roadName()) + .isOpened(capsuleSummaryDto.isOpened()) + .createdAt(capsuleSummaryDto.createdAt()) + .build(); + } +} 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/reqeust/GroupCapsuleCreateRequest.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/reqeust/GroupCapsuleCreateRequest.java index 7d9b12843..7754b3ae0 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/reqeust/GroupCapsuleCreateRequest.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/reqeust/GroupCapsuleCreateRequest.java @@ -14,6 +14,10 @@ @Schema(description = "그룹 캡슐 생성 포맷") public record GroupCapsuleCreateRequest( + @Schema(description = "그룹 멤버 아이디들") + @NotNull(message = "그룹 멤버 아이디들는 필수입니다.") + List groupMemberIds, + @Schema(description = "업로드한 이미지 경로 ex) xxx.jpg") List<@Image String> imageNames, @@ -52,6 +56,7 @@ public record GroupCapsuleCreateRequest( public GroupCapsuleCreateRequestDto toGroupCapsuleCreateRequestDto() { return GroupCapsuleCreateRequestDto.builder() + .groupMemberIds(groupMemberIds) .videoNames(videoNames) .imageNames(imageNames) .capsuleSkinId(capsuleSkinId) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleDetailResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleDetailResponse.java index 1a038c43b..378ec4751 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleDetailResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleDetailResponse.java @@ -3,10 +3,19 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import java.util.List; +import java.util.function.Function; +import lombok.Builder; +import org.locationtech.jts.geom.Point; +import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleDetailDto; import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @Schema(description = "그룹 캡슐 상세 정보") +@Builder public record GroupCapsuleDetailResponse( + @Schema(description = "캡슐 아이디") + Long capsuleId, @Schema(description = "캡슐 스킨 url") String capsuleSkinUrl, @@ -20,23 +29,59 @@ public record GroupCapsuleDetailResponse( @Schema(description = "생성자 닉네임") String nickname, - @Schema(description = "캡슐 생성일") + @Schema(description = "생성자 프로필 url") + String profileUrl, + + @Schema(description = "생성일") ZonedDateTime createdDate, + @Schema(description = "캡슐 위도 좌표") + Double latitude, + + @Schema(description = "캡슐 경도 좌표") + Double longitude, + @Schema(description = "캡슐 생성 주소") String address, + @Schema(description = "캡슐 생성 도로 이름") + String roadName, + @Schema(description = "제목") String title, @Schema(description = "내용") String content, - @Schema(description = "미디어 url들") - List mediaUrls, + @Schema(description = "이미지 url들") + List imageUrls, + + @Schema(description = "비디오 url들") + List videoUrls, @Schema(description = "개봉 여부") - Boolean isOpened + Boolean isOpened, + + @Schema(description = "캡슐 타입") + CapsuleType capsuleType ) { + public GroupCapsuleDetailResponse { + if (dueDate != null) { + dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + + createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + + public static GroupCapsuleDetailResponse createOf( + final GroupCapsuleDetailDto groupCapsuleDetailDto, + final Function singlePreSignUrlFunction, + final Function> multiplePreSignUrlFunction, + final Function changePointFunction + ) { + return groupCapsuleDetailDto.toResponse(singlePreSignUrlFunction, + multiplePreSignUrlFunction, changePointFunction); + } + } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleSummaryResponse.java index a9dd3fda7..4337b2879 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/data/response/GroupCapsuleSummaryResponse.java @@ -2,27 +2,68 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; +import java.util.List; +import java.util.function.Function; +import lombok.Builder; +import org.locationtech.jts.geom.Point; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleSummaryDto; +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; -@Schema(description = "그룹 캡슐 요약 정보") +@Schema(description = "캡슐 요약 정보") +@Builder public record GroupCapsuleSummaryResponse( - @Schema(description = "캡슐 아이디") - Long id, + @Schema(description = "그룹원 요약 정보") + List members, @Schema(description = "생성자 닉네임") String nickname, + @Schema(description = "생성자 프로필 url") + String profileUrl, + @Schema(description = "캡슐 스킨 url") String skinUrl, @Schema(description = "제목") String title, - @Schema(description = "생성일") + @Schema(description = "개봉일") ZonedDateTime dueDate, + @Schema(description = "캡슐 위도 좌표") + Double latitude, + + @Schema(description = "캡슐 경도 좌표") + Double longitude, + @Schema(description = "캡슐 생성 주소") - String address + String address, + + @Schema(description = "캡슐 생성 도로 이름") + String roadName, + + @Schema(description = "개봉 여부") + Boolean isOpened, + + @Schema(description = "캡슐 생성 일") + ZonedDateTime createdAt ) { -} \ No newline at end of file + public GroupCapsuleSummaryResponse { + if (dueDate != null) { + dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + + public static GroupCapsuleSummaryResponse createOf( + final GroupCapsuleSummaryDto summaryDto, + final Function preSignUrlFunction, + final Function changePointFunction + ) { + return summaryDto.toResponse(preSignUrlFunction, changePointFunction); + } +} 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/facade/GroupCapsuleFacade.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/facade/GroupCapsuleFacade.java index 8ee3eae83..ba2164109 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/facade/GroupCapsuleFacade.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/facade/GroupCapsuleFacade.java @@ -8,6 +8,7 @@ import site.timecapsulearchive.core.domain.capsule.generic_capsule.service.ImageService; import site.timecapsulearchive.core.domain.capsule.generic_capsule.service.VideoService; import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleCreateRequestDto; +import site.timecapsulearchive.core.domain.capsule.group_capsule.service.GroupCapsuleOpenService; import site.timecapsulearchive.core.domain.capsule.group_capsule.service.GroupCapsuleService; import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; import site.timecapsulearchive.core.domain.capsuleskin.service.CapsuleSkinService; @@ -28,6 +29,7 @@ public class GroupCapsuleFacade { private final VideoService videoService; private final GeoTransformManager geoTransformManager; private final CapsuleSkinService capsuleSkinService; + private final GroupCapsuleOpenService groupCapsuleOpenService; @Transactional public void saveGroupCapsule( @@ -48,5 +50,7 @@ public void saveGroupCapsule( imageService.bulkSave(dto.imageNames(), capsule, member); videoService.bulkSave(dto.videoNames(), capsule, member); + + groupCapsuleOpenService.bulkSave(dto.groupMemberIds(), capsule); } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenQueryRepository.java new file mode 100644 index 000000000..5a9bacd2b --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenQueryRepository.java @@ -0,0 +1,46 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.repository; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import site.timecapsulearchive.core.domain.capsule.entity.Capsule; + +@Repository +@RequiredArgsConstructor +public class GroupCapsuleOpenQueryRepository { + + private final JdbcTemplate jdbcTemplate; + + public void bulkSave(final List groupMemberIds, final Capsule capsule) { + jdbcTemplate.batchUpdate( + """ + INSERT INTO group_capsule_open ( + group_capsule_open_id, is_opened, member_id, capsule_id, created_at, updated_at + ) values (?, ? ,? ,? ,?, ?) + """, + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setNull(1, Types.BIGINT); + ps.setBoolean(2, false); + ps.setLong(3, groupMemberIds.get(i)); + ps.setLong(4, capsule.getId()); + ps.setTimestamp(5, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + ps.setTimestamp(6, Timestamp.valueOf(ZonedDateTime.now().toLocalDateTime())); + } + + @Override + public int getBatchSize() { + return groupMemberIds.size(); + } + } + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenRepository.java new file mode 100644 index 000000000..a46f6209c --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleOpenRepository.java @@ -0,0 +1,8 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.repository; + +import org.springframework.data.repository.Repository; +import site.timecapsulearchive.core.domain.capsule.entity.GroupCapsuleOpen; + +public interface GroupCapsuleOpenRepository extends Repository { + +} 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 new file mode 100644 index 000000000..047d86fff --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepository.java @@ -0,0 +1,169 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.repository; + +import static com.querydsl.core.group.GroupBy.groupBy; +import static com.querydsl.core.group.GroupBy.list; +import static site.timecapsulearchive.core.domain.capsule.entity.QCapsule.capsule; +import static site.timecapsulearchive.core.domain.capsule.entity.QGroupCapsuleOpen.groupCapsuleOpen; +import static site.timecapsulearchive.core.domain.capsule.entity.QImage.image; +import static site.timecapsulearchive.core.domain.capsule.entity.QVideo.video; +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.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; +import site.timecapsulearchive.core.domain.member.entity.QMember; + +@Repository +@RequiredArgsConstructor +public class GroupCapsuleQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Optional findGroupCapsuleDetailDtoByCapsuleId( + final Long capsuleId + ) { + final QMember owner = new QMember("owner"); + final QMember groupMember = new QMember("groupMember"); + + return Optional.ofNullable(jpaQueryFactory + .selectFrom(capsule) + .join(owner).on(capsule.member.id.eq(owner.id)) + .join(capsuleSkin).on(capsule.capsuleSkin.id.eq(capsuleSkin.id)) + .leftJoin(image).on(capsule.id.eq(image.capsule.id)) + .leftJoin(video).on(capsule.id.eq(video.capsule.id)) + .join(groupCapsuleOpen).on(groupCapsuleOpen.capsule.id.eq(capsuleId)) + .join(groupMember).on(groupMember.id.eq(groupCapsuleOpen.member.id)) + .groupBy(groupCapsuleOpen.id) + .where(groupCapsuleOpen.capsule.id.eq(capsuleId)) + .where(capsule.id.eq(capsuleId).and(capsule.type.eq(CapsuleType.GROUP))) + .transform( + groupBy(capsule.id).as( + Projections.constructor( + GroupCapsuleDetailDto.class, + Projections.constructor( + CapsuleDetailDto.class, + capsule.id, + capsuleSkin.imageUrl, + capsule.dueDate, + owner.nickname, + owner.profileUrl, + capsule.createdAt, + capsule.point, + capsule.address.fullRoadAddressName, + capsule.address.roadName, + capsule.title, + capsule.content, + groupConcatDistinct(image.imageUrl), + groupConcatDistinct(video.videoUrl), + capsule.isOpened, + capsule.type + ), + list(Projections.constructor(GroupMemberSummaryDto.class, + groupMember.nickname, + groupMember.profileUrl, + groupCapsuleOpen.isOpened) + ) + ) + ) + ).get(capsuleId)); + } + + private StringExpression groupConcatDistinct(final StringExpression expression) { + return Expressions.stringTemplate("GROUP_CONCAT(DISTINCT {0})", expression); + } + + public Optional findGroupCapsuleSummaryDtoByCapsuleId( + final Long capsuleId) { + final QMember owner = new QMember("owner"); + final QMember groupMember = new QMember("groupMember"); + + return Optional.ofNullable(jpaQueryFactory + .selectFrom(capsule) + .join(owner).on(capsule.member.id.eq(owner.id)) + .join(capsuleSkin).on(capsule.capsuleSkin.id.eq(capsuleSkin.id)).join(groupCapsuleOpen) + .on(groupCapsuleOpen.capsule.id.eq(capsuleId)) + .join(groupMember).on(groupMember.id.eq(groupCapsuleOpen.member.id)) + .groupBy(groupCapsuleOpen.id) + .where(groupCapsuleOpen.capsule.id.eq(capsuleId)) + .where(capsule.id.eq(capsuleId).and(capsule.type.eq(CapsuleType.GROUP))) + .transform( + groupBy(capsule.id).as( + Projections.constructor( + GroupCapsuleSummaryDto.class, + Projections.constructor( + CapsuleSummaryDto.class, + owner.nickname, + owner.profileUrl, + capsuleSkin.imageUrl, + capsule.title, + capsule.dueDate, + capsule.point, + capsule.address.fullRoadAddressName, + capsule.address.roadName, + capsule.isOpened, + capsule.createdAt + ), + list(Projections.constructor(GroupMemberSummaryDto.class, + groupMember.nickname, + groupMember.profileUrl, + groupCapsuleOpen.isOpened) + ) + ) + + ) + ).get(capsuleId)); + } + + 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/GroupCapsuleOpenService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleOpenService.java new file mode 100644 index 000000000..32496954d --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleOpenService.java @@ -0,0 +1,18 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import site.timecapsulearchive.core.domain.capsule.entity.Capsule; +import site.timecapsulearchive.core.domain.capsule.group_capsule.repository.GroupCapsuleOpenQueryRepository; + +@Service +@RequiredArgsConstructor +public class GroupCapsuleOpenService { + + private final GroupCapsuleOpenQueryRepository repository; + + public void bulkSave(final List groupMemberIds, final Capsule capsule) { + repository.bulkSave(groupMemberIds, capsule); + } +} 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 7e87c8487..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 @@ -1,13 +1,21 @@ package site.timecapsulearchive.core.domain.capsule.group_capsule.service; +import java.time.ZoneOffset; +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; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.exception.CapsuleNotFondException; import site.timecapsulearchive.core.domain.capsule.generic_capsule.repository.CapsuleRepository; 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; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -18,6 +26,7 @@ public class GroupCapsuleService { private final CapsuleRepository capsuleRepository; + private final GroupCapsuleQueryRepository groupCapsuleQueryRepository; @Transactional public Capsule saveGroupCapsule( @@ -34,4 +43,50 @@ public Capsule saveGroupCapsule( return capsule; } + + public GroupCapsuleDetailDto findGroupCapsuleDetailByGroupIdAndCapsuleId( + final Long capsuleId + ) { + final GroupCapsuleDetailDto detailDto = groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId( + capsuleId) + .orElseThrow(CapsuleNotFondException::new); + + if (capsuleNotOpened(detailDto)) { + return detailDto.excludeDetailContents(); + } + + return detailDto; + } + + private boolean capsuleNotOpened(final GroupCapsuleDetailDto detailDto) { + if (detailDto.capsuleDetailDto().dueDate() == null) { + return false; + } + + return !detailDto.capsuleDetailDto().isOpened() || detailDto.capsuleDetailDto().dueDate() + .isAfter(ZonedDateTime.now(ZoneOffset.UTC)); + } + + public GroupCapsuleSummaryDto findGroupCapsuleSummaryByGroupIDAndCapsuleId( + final Long capsuleId) { + 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 484404a9d..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 @@ -48,14 +72,17 @@ public ResponseEntity> getPublicCapsuleSummaryBy return ResponseEntity.ok( ApiSpec.success( SuccessCode.SUCCESS, - CapsuleSummaryResponse.createOf(summaryDto, - s3PreSignedUrlManager::getS3PreSignedUrlForGet) + CapsuleSummaryResponse.createOf( + summaryDto, + s3PreSignedUrlManager::getS3PreSignedUrlForGet, + geoTransformManager::changePoint3857To4326 + ) ) ); } @GetMapping( - value = "/capsules/{capsule_id}/detail", + value = "/{capsule_id}/detail", produces = {"application/json"} ) @Override @@ -72,18 +99,19 @@ public ResponseEntity> getPublicCapsuleDetailById CapsuleDetailResponse.createOf( detailDto, s3PreSignedUrlManager::getS3PreSignedUrlForGet, - s3PreSignedUrlManager::getS3PreSignedUrlsForGet + s3PreSignedUrlManager::getS3PreSignedUrlsForGet, + geoTransformManager::changePoint3857To4326 ) ) ); } - @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); @@ -102,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/dto/PublicCapsuleDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/PublicCapsuleDetailDto.java index 6e724118f..84a3f0f40 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/PublicCapsuleDetailDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/dto/PublicCapsuleDetailDto.java @@ -34,23 +34,23 @@ public PublicCapsuleDetailResponse toResponse( final List preSignedImageUrls = multiplePreSignUrlFunction.apply(images); final List preSignedVideoUrls = multiplePreSignUrlFunction.apply(videos); - return new PublicCapsuleDetailResponse( - capsuleId, - singlePreSignUrlFunction.apply(capsuleSkinUrl), - dueDate, - nickname, - profileUrl, - createdAt, - changePoint.getX(), - changePoint.getY(), - address, - roadName, - title, - content, - preSignedImageUrls, - preSignedVideoUrls, - isOpened, - capsuleType - ); + return PublicCapsuleDetailResponse.builder() + .capsuleId(capsuleId) + .capsuleSkinUrl(singlePreSignUrlFunction.apply(capsuleSkinUrl)) + .dueDate(dueDate) + .nickname(nickname) + .profileUrl(profileUrl) + .createdDate(createdAt) + .latitude(changePoint.getX()) + .longitude(changePoint.getY()) + .address(address) + .roadName(roadName) + .title(title) + .content(content) + .imageUrls(preSignedImageUrls) + .videoUrls(preSignedVideoUrls) + .isOpened(isOpened) + .capsuleType(capsuleType) + .build(); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/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/data/response/PublicCapsuleDetailResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/PublicCapsuleDetailResponse.java index 3a6c45ed9..606a03015 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/PublicCapsuleDetailResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/data/response/PublicCapsuleDetailResponse.java @@ -3,10 +3,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import java.util.List; -import java.util.function.Function; import lombok.Builder; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; -import site.timecapsulearchive.core.domain.capsule.public_capsule.data.dto.PublicCapsuleDetailDto; import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @Schema(description = "공개 캡슐 상세 정보") @@ -67,36 +65,8 @@ public record PublicCapsuleDetailResponse( dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } - createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); - } - - public static PublicCapsuleDetailResponse createOf( - final PublicCapsuleDetailDto detailDto, - final Function singlePreSignUrlFunction, - final Function> multiplePreSignUrlFunction - ) { - final List preSignedImageUrls = multiplePreSignUrlFunction.apply( - detailDto.images()); - final List preSignedVideoUrls = multiplePreSignUrlFunction.apply( - detailDto.videos()); - - return new PublicCapsuleDetailResponse( - detailDto.capsuleId(), - singlePreSignUrlFunction.apply(detailDto.capsuleSkinUrl()), - detailDto.dueDate(), - detailDto.nickname(), - detailDto.profileUrl(), - detailDto.createdAt(), - detailDto.point().getX(), - detailDto.point().getY(), - detailDto.address(), - detailDto.roadName(), - detailDto.title(), - detailDto.content(), - preSignedImageUrls, - preSignedVideoUrls, - detailDto.isOpened(), - detailDto.capsuleType() - ); + if (createdDate != null) { + createdDate = createdDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/repository/PublicCapsuleQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/public_capsule/repository/PublicCapsuleQueryRepository.java index 9b00c8ff3..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 @@ -44,6 +45,7 @@ public Optional findPublicCapsuleDetailDtosByMemberIdAndCapsul member.nickname, member.profileUrl, capsule.createdAt, + capsule.point, capsule.address.fullRoadAddressName, capsule.address.roadName, capsule.title, @@ -90,6 +92,7 @@ public Optional findPublicCapsuleSummaryDtosByMemberIdAndCaps capsuleSkin.imageUrl, capsule.title, capsule.dueDate, + capsule.point, capsule.address.fullRoadAddressName, capsule.address.roadName, capsule.isOpened, @@ -167,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..e58b91da6 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,20 @@ 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 903aa0db0..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,30 +10,54 @@ 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; import site.timecapsulearchive.core.domain.capsule.secret_capsule.service.SecretCapsuleService; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.common.response.SuccessCode; +import site.timecapsulearchive.core.global.geography.GeoTransformManager; 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, @@ -51,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, @@ -64,12 +89,16 @@ public ResponseEntity> getSecretCapsuleSummary( return ResponseEntity.ok( ApiSpec.success( SuccessCode.SUCCESS, - CapsuleSummaryResponse.createOf(dto, s3PreSignedUrlManager::getS3PreSignedUrlForGet) + CapsuleSummaryResponse.createOf( + dto, + s3PreSignedUrlManager::getS3PreSignedUrlForGet, + geoTransformManager::changePoint3857To4326 + ) ) ); } - @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, @@ -84,13 +113,14 @@ public ResponseEntity> getSecretCapsuleDetail( CapsuleDetailResponse.createOf( dto, s3PreSignedUrlManager::getS3PreSignedUrlForGet, - s3PreSignedUrlManager::getS3PreSignedUrlsForGet + s3PreSignedUrlManager::getS3PreSignedUrlsForGet, + geoTransformManager::changePoint3857To4326 ) ) ); } - @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/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java index 3f22e5503..9b3c37704 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/dto/MySecreteCapsuleDto.java @@ -16,14 +16,6 @@ public record MySecreteCapsuleDto( CapsuleType type ) { - public MySecreteCapsuleDto { - if (dueDate != null) { - dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); - } - - createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); - } - public MySecreteCapsuleResponse toResponse( final Function preSignUrlFunction ) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java index 66a287a09..15314fed7 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/data/response/MySecreteCapsuleResponse.java @@ -4,6 +4,7 @@ import java.time.ZonedDateTime; import lombok.Builder; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @Builder @Schema(description = "내 비밀 캡슐 응답") @@ -30,4 +31,13 @@ public record MySecreteCapsuleResponse( CapsuleType type ) { + public MySecreteCapsuleResponse { + if (dueDate != null) { + dueDate = dueDate.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/repository/SecretCapsuleQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/repository/SecretCapsuleQueryRepository.java index 5cffbf1b7..99490a193 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/repository/SecretCapsuleQueryRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsule/secret_capsule/repository/SecretCapsuleQueryRepository.java @@ -43,6 +43,7 @@ public Optional findSecretCapsuleSummaryDtosByMemberIdAndCaps capsule.capsuleSkin.imageUrl, capsule.title, capsule.dueDate, + capsule.point, capsule.address.fullRoadAddressName, capsule.address.roadName, capsule.isOpened, @@ -70,6 +71,7 @@ public Optional findSecretCapsuleDetailDtosByMemberIdAndCapsul member.nickname, member.profileUrl, capsule.createdAt, + capsule.point, capsule.address.fullRoadAddressName, capsule.address.roadName, capsule.title, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java index 2241b2397..35bef1e4b 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/capsuleskin/data/response/CapsuleSkinSummaryResponse.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import lombok.Builder; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; @Schema(description = "캡슐 스킨 요약 정보") @Builder @@ -20,4 +21,9 @@ public record CapsuleSkinSummaryResponse( ZonedDateTime createdAt ) { + public CapsuleSkinSummaryResponse { + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java index 68519a9fd..436c43636 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApi.java @@ -23,32 +23,48 @@ public interface FriendApi { @Operation( - summary = "소셜 친구 요청 수락", - description = "상대방으로부터 온 친구 요청을 수락한다.", + summary = "소셜 친구 목록 조회", + description = "사용자의 소셜 친구 목록을 보여준다.", security = {@SecurityRequirement(name = "user_token")}, tags = {"friend"} ) @ApiResponses(value = { @ApiResponse( responseCode = "200", - description = "처리 완료" - ), - @ApiResponse( - responseCode = "404", - description = "친구 요청이 존재하지 않는 경우 발생" - ), - @ApiResponse( - responseCode = "500", - description = "외부 API 요청 실패" + description = "ok" ) }) - ResponseEntity> acceptFriendRequest( + ResponseEntity> findFriends( Long memberId, - @Parameter(in = ParameterIn.PATH, required = true, schema = @Schema()) - Long friendId + @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true) + int size, + + @Parameter(in = ParameterIn.QUERY, description = "마지막 데이터의 시간", required = true) + ZonedDateTime createdAt ); + @Operation( + summary = "소셜 친구 요청 목록 조회", + description = "사용자의 소셜 친구 요청 목록을 보여준다. 수락 대기 중인 요청만 해당한다.", + security = {@SecurityRequirement(name = "user_token")}, + tags = {"friend"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "ok" + ) + }) + ResponseEntity> findFriendRequests( + Long memberId, + + @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true) + int size, + + @Parameter(in = ParameterIn.QUERY, description = "마지막 데이터의 시간", required = true) + ZonedDateTime createdAt + ); @Operation( summary = "소셜 친구 삭제", @@ -96,54 +112,33 @@ ResponseEntity> denyFriendRequest( Long friendId ); - @Operation( - summary = "소셜 친구 목록 조회", - description = "사용자의 소셜 친구 목록을 보여준다.", - security = {@SecurityRequirement(name = "user_token")}, - tags = {"friend"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "ok" - ) - }) - ResponseEntity> findFriends( - Long memberId, - - @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true) - int size, - - @Parameter(in = ParameterIn.QUERY, description = "마지막 데이터의 시간", required = true) - ZonedDateTime createdAt - ); @Operation( - summary = "소셜 친구 요청 목록 조회", - description = "사용자의 소셜 친구 요청 목록을 보여준다. 수락 대기 중인 요청만 해당한다.", + summary = "소셜 친구 요청", + description = "사용자에게 친구 요청을 보낸다. 해당 사용자에게 알림이 발송된다.", security = {@SecurityRequirement(name = "user_token")}, tags = {"friend"} ) @ApiResponses(value = { @ApiResponse( - responseCode = "200", - description = "ok" + responseCode = "202", + description = "처리 시작" + ), + @ApiResponse( + responseCode = "500", + description = "외부 API 요청 실패" ) }) - ResponseEntity> findFriendRequests( + ResponseEntity> requestFriend( Long memberId, - @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true) - int size, - - @Parameter(in = ParameterIn.QUERY, description = "마지막 데이터의 시간", required = true) - ZonedDateTime createdAt + @Parameter(in = ParameterIn.PATH, description = "친구 아이디", required = true, schema = @Schema()) + Long friendId ); - @Operation( - summary = "소셜 친구 요청", - description = "사용자에게 친구 요청을 보낸다. 해당 사용자에게 알림이 발송된다.", + summary = "소셜 친구들에게 친구 요청", + description = "사용자들에게 친구 요청을 보낸다. 해당 사용자들에게 알림이 발송된다.", security = {@SecurityRequirement(name = "user_token")}, tags = {"friend"} ) @@ -157,33 +152,37 @@ ResponseEntity> findFriendRequests( description = "외부 API 요청 실패" ) }) - ResponseEntity> requestFriend( + ResponseEntity> requestFriends( Long memberId, - @Parameter(in = ParameterIn.PATH, description = "친구 아이디", required = true, schema = @Schema()) - Long friendId + SendFriendRequest request ); @Operation( - summary = "소셜 친구들에게 친구 요청", - description = "사용자들에게 친구 요청을 보낸다. 해당 사용자들에게 알림이 발송된다.", + summary = "소셜 친구 요청 수락", + description = "상대방으로부터 온 친구 요청을 수락한다.", security = {@SecurityRequirement(name = "user_token")}, tags = {"friend"} ) @ApiResponses(value = { @ApiResponse( - responseCode = "202", - description = "처리 시작" + responseCode = "200", + description = "처리 완료" + ), + @ApiResponse( + responseCode = "404", + description = "친구 요청이 존재하지 않는 경우 발생" ), @ApiResponse( responseCode = "500", description = "외부 API 요청 실패" ) }) - ResponseEntity> requestFriends( + ResponseEntity> acceptFriendRequest( Long memberId, - SendFriendRequest request + @Parameter(in = ParameterIn.PATH, required = true, schema = @Schema()) + Long friendId ); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java index 0e188c9ad..56c2e0755 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/api/FriendApiController.java @@ -40,18 +40,6 @@ public class FriendApiController implements FriendApi { private final FriendFacade friendFacade; private final HashEncryptionManager hashEncryptionManager; - - @PostMapping(value = "/{friend_id}/accept-request") - @Override - public ResponseEntity> acceptFriendRequest( - @AuthenticationPrincipal final Long memberId, - @PathVariable("friend_id") final Long friendId - ) { - friendService.acceptFriend(memberId, friendId); - - return ResponseEntity.ok(ApiSpec.empty(SuccessCode.SUCCESS)); - } - @GetMapping @Override public ResponseEntity> findFriends( @@ -144,6 +132,17 @@ public ResponseEntity> requestFriends( .body(ApiSpec.empty(SuccessCode.ACCEPTED)); } + @PostMapping(value = "/{friend_id}/accept-request") + @Override + public ResponseEntity> acceptFriendRequest( + @AuthenticationPrincipal final Long memberId, + @PathVariable("friend_id") final Long friendId + ) { + friendService.acceptFriend(memberId, friendId); + + return ResponseEntity.ok(ApiSpec.empty(SuccessCode.SUCCESS)); + } + @PostMapping( value = "/search/phone", produces = {"application/json"}, diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchFriendSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchFriendSummaryDto.java index 3680bbdcd..6ca42ab1c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchFriendSummaryDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchFriendSummaryDto.java @@ -11,7 +11,8 @@ public record SearchFriendSummaryDto( String nickname, ByteArrayWrapper phoneHash, Boolean isFriend, - Boolean isFriendRequest + Boolean isFriendInviteToFriend, + Boolean isFriendInviteToMe ) { public SearchFriendSummaryResponse toResponse( @@ -24,7 +25,8 @@ public SearchFriendSummaryResponse toResponse( .nickname(nickname) .phone(phoneBook.originPhone()) .isFriend(isFriend) - .isFriendRequest(isFriendRequest) + .isFriendInviteToFriend(isFriendInviteToFriend) + .isFriendInviteToMe(isFriendInviteToMe) .build(); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchTagFriendSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchFriendSummaryDtoByTag.java similarity index 70% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchTagFriendSummaryDto.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchFriendSummaryDtoByTag.java index 3f7973e81..859905694 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchTagFriendSummaryDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/dto/SearchFriendSummaryDtoByTag.java @@ -4,12 +4,13 @@ import site.timecapsulearchive.core.domain.friend.data.response.SearchTagFriendSummaryResponse; @Builder -public record SearchTagFriendSummaryDto( +public record SearchFriendSummaryDtoByTag( Long id, String profileUrl, String nickname, Boolean isFriend, - Boolean isFriendRequest + Boolean isFriendInviteToFriend, + Boolean isFriendInviteToMe ) { public SearchTagFriendSummaryResponse toResponse() { @@ -18,7 +19,8 @@ public SearchTagFriendSummaryResponse toResponse() { .profileUrl(profileUrl) .nickname(nickname) .isFriend(isFriend) - .isFriendRequest(isFriendRequest) + .isFriendInviteToFriend(isFriendInviteToFriend) + .isFriendInviteToMe(isFriendInviteToMe) .build(); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java index bd740b365..a70e24f4d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/FriendSummaryResponse.java @@ -24,7 +24,7 @@ public record FriendSummaryResponse( public FriendSummaryResponse { if (createdAt != null) { - createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); } } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchFriendSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchFriendSummaryResponse.java index ede1c5959..f61b2cc04 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchFriendSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchFriendSummaryResponse.java @@ -25,8 +25,11 @@ public record SearchFriendSummaryResponse( @Schema(description = "친구 유무") Boolean isFriend, - @Schema(description = "친구 요청 유무") - Boolean isFriendRequest + @Schema(description = "친구한테 요청 유무") + Boolean isFriendInviteToFriend, + + @Schema(description = "친구로부터 요청 유무") + Boolean isFriendInviteToMe ) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchTagFriendSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchTagFriendSummaryResponse.java index 6d394e284..1afe2becb 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchTagFriendSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/data/response/SearchTagFriendSummaryResponse.java @@ -19,9 +19,11 @@ public record SearchTagFriendSummaryResponse( @Schema(description = "친구 유무") Boolean isFriend, - @Schema(description = "친구 요청 유무") - Boolean isFriendRequest + @Schema(description = "친구한테 요청 유무") + Boolean isFriendInviteToFriend, + @Schema(description = "친구로부터 요청 유무") + Boolean isFriendInviteToMe ) { } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/DuplicateFriendIdException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendDuplicateIdException.java similarity index 71% rename from backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/DuplicateFriendIdException.java rename to backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendDuplicateIdException.java index 9c8afdb4e..d221257d9 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/DuplicateFriendIdException.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendDuplicateIdException.java @@ -3,9 +3,9 @@ import site.timecapsulearchive.core.global.error.ErrorCode; import site.timecapsulearchive.core.global.error.exception.BusinessException; -public class DuplicateFriendIdException extends BusinessException { +public class FriendDuplicateIdException extends BusinessException { - public DuplicateFriendIdException() { + public FriendDuplicateIdException() { super(ErrorCode.FRIEND_DUPLICATE_ID_ERROR); } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendTwoWayInviteException.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendTwoWayInviteException.java new file mode 100644 index 000000000..b502c6429 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/exception/FriendTwoWayInviteException.java @@ -0,0 +1,11 @@ +package site.timecapsulearchive.core.domain.friend.exception; + +import site.timecapsulearchive.core.global.error.ErrorCode; +import site.timecapsulearchive.core.global.error.exception.BusinessException; + +public class FriendTwoWayInviteException extends BusinessException { + + public FriendTwoWayInviteException() { + super(ErrorCode.FRIEND_TWO_WAY_INVITE_ERROR); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java index d5b7ca046..c7c94a40a 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteRepository.java @@ -22,7 +22,7 @@ List findFriendInviteWithMembersByOwnerIdAndFriendId( @Param(value = "friendId") Long friendId ); - Optional findFriendInviteByOwnerIdAndFriendId(Long memberId, Long friendId); + Optional findFriendInviteByOwnerIdAndFriendId(Long memberId, Long targetId); void deleteFriendInviteById(Long id); 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 4b0ba3452..412deff3f 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 @@ -16,7 +16,8 @@ import org.springframework.stereotype.Repository; import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; -import site.timecapsulearchive.core.domain.friend.data.dto.SearchTagFriendSummaryDto; +import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; +import site.timecapsulearchive.core.domain.friend.entity.QFriendInvite; import site.timecapsulearchive.core.global.common.wrapper.ByteArrayWrapper; @Repository @@ -88,6 +89,9 @@ public List findFriendsByPhone( final Long memberId, final List hashes ) { + final QFriendInvite friendInviteToFriend = new QFriendInvite("friendInviteToFriend"); + final QFriendInvite friendInviteToMe = new QFriendInvite("friendInviteToMe"); + return jpaQueryFactory .select( Projections.constructor( @@ -100,40 +104,61 @@ public List findFriendsByPhone( member.phone_hash ), memberFriend.id.isNotNull(), - friendInvite.id.isNotNull() + friendInviteToFriend.id.isNotNull(), + friendInviteToMe.id.isNotNull() ) ) .from(member) .leftJoin(memberFriend) .on(memberFriend.friend.id.eq(member.id).and(memberFriend.owner.id.eq(memberId))) - .leftJoin(friendInvite) - .on(friendInvite.friend.id.eq(member.id).and(friendInvite.owner.id.eq(memberId))) + .leftJoin(friendInviteToFriend) + .on(friendInviteToFriend.friend.id.eq(member.id) + .and(friendInviteToFriend.owner.id.eq(memberId))) + .leftJoin(friendInviteToMe) + .on(friendInviteToMe.friend.id.eq(memberId) + .and(friendInviteToMe.owner.id.eq(member.id))) .where(member.phone_hash.in(hashes)) .fetch(); } - public Optional findFriendsByTag( + public Optional findFriendsByTag( final Long memberId, final String tag ) { + final QFriendInvite friendInviteToFriend = new QFriendInvite("friendInviteToFriend"); + final QFriendInvite friendInviteToMe = new QFriendInvite("friendInviteToMe"); + return Optional.ofNullable(jpaQueryFactory .select( Projections.constructor( - SearchTagFriendSummaryDto.class, + SearchFriendSummaryDtoByTag.class, member.id, member.profileUrl, member.nickname, memberFriend.id.isNotNull(), - friendInvite.id.isNotNull() + friendInviteToFriend.id.isNotNull(), + friendInviteToMe.id.isNotNull() ) ) .from(member) .leftJoin(memberFriend) .on(memberFriend.friend.id.eq(member.id).and(memberFriend.owner.id.eq(memberId))) - .leftJoin(friendInvite) - .on(friendInvite.friend.id.eq(member.id).and(friendInvite.owner.id.eq(memberId))) + .leftJoin(friendInviteToFriend) + .on(friendInviteToFriend.friend.id.eq(member.id) + .and(friendInviteToFriend.owner.id.eq(memberId))) + .leftJoin(friendInviteToMe) + .on(friendInviteToMe.friend.id.eq(memberId) + .and(friendInviteToMe.owner.id.eq(member.id))) .where(member.tag.eq(tag)) .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/domain/friend/service/FriendService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java index 5a0512ba9..913700af2 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/friend/service/FriendService.java @@ -2,6 +2,7 @@ import java.time.ZonedDateTime; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -11,14 +12,15 @@ import org.springframework.transaction.support.TransactionTemplate; import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; -import site.timecapsulearchive.core.domain.friend.data.dto.SearchTagFriendSummaryDto; +import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; import site.timecapsulearchive.core.domain.friend.data.response.FriendReqStatusResponse; import site.timecapsulearchive.core.domain.friend.data.response.SearchTagFriendSummaryResponse; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; -import site.timecapsulearchive.core.domain.friend.exception.DuplicateFriendIdException; +import site.timecapsulearchive.core.domain.friend.exception.FriendDuplicateIdException; import site.timecapsulearchive.core.domain.friend.exception.FriendInviteNotFoundException; import site.timecapsulearchive.core.domain.friend.exception.FriendNotFoundException; +import site.timecapsulearchive.core.domain.friend.exception.FriendTwoWayInviteException; import site.timecapsulearchive.core.domain.friend.repository.FriendInviteQueryRepository; import site.timecapsulearchive.core.domain.friend.repository.FriendInviteRepository; import site.timecapsulearchive.core.domain.friend.repository.MemberFriendQueryRepository; @@ -42,21 +44,21 @@ public class FriendService { private final TransactionTemplate transactionTemplate; public FriendReqStatusResponse requestFriend(final Long memberId, final Long friendId) { - if (memberId.equals(friendId)) { - throw new DuplicateFriendIdException(); - } + validateFriendDuplicateId(memberId, friendId); + validateTwoWayInvite(memberId, friendId); + final Member owner = memberRepository.findMemberById(memberId).orElseThrow( MemberNotFoundException::new); final Member friend = memberRepository.findMemberById(friendId).orElseThrow( MemberNotFoundException::new); - final FriendInvite friendInvite = FriendInvite.createOf(owner, friend); + final FriendInvite createfriendInvite = FriendInvite.createOf(owner, friend); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { - friendInviteRepository.save(friendInvite); + friendInviteRepository.save(createfriendInvite); } }); @@ -65,10 +67,24 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { return FriendReqStatusResponse.success(); } - public void acceptFriend(final Long memberId, final Long friendId) { + private void validateFriendDuplicateId(final Long memberId, final Long friendId) { if (memberId.equals(friendId)) { - throw new DuplicateFriendIdException(); + throw new FriendDuplicateIdException(); + } + } + + private void validateTwoWayInvite(final Long memberId, final Long friendId) { + final Optional friendInvite = friendInviteRepository.findFriendInviteByOwnerIdAndFriendId( + friendId, memberId); + + if (friendInvite.isPresent()) { + throw new FriendTwoWayInviteException(); } + } + + + public void acceptFriend(final Long memberId, final Long friendId) { + validateFriendDuplicateId(memberId, friendId); final String[] ownerNickname = new String[1]; transactionTemplate.execute(new TransactionCallbackWithoutResult() { @@ -99,9 +115,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { @Transactional public void denyRequestFriend(Long memberId, Long friendId) { - if (memberId.equals(friendId)) { - throw new DuplicateFriendIdException(); - } + validateFriendDuplicateId(memberId, friendId); final FriendInvite friendInvite = friendInviteRepository .findFriendInviteByOwnerIdAndFriendId(memberId, friendId).orElseThrow( FriendNotFoundException::new); @@ -111,9 +125,7 @@ public void denyRequestFriend(Long memberId, Long friendId) { @Transactional public void deleteFriend(final Long memberId, final Long friendId) { - if (memberId.equals(friendId)) { - throw new DuplicateFriendIdException(); - } + validateFriendDuplicateId(memberId, friendId); List memberFriends = memberFriendRepository .findMemberFriendByOwnerIdAndFriendId(memberId, friendId); @@ -153,7 +165,7 @@ public List findFriendsByPhone( @Transactional(readOnly = true) public SearchTagFriendSummaryResponse searchFriend(final Long memberId, final String tag) { - final SearchTagFriendSummaryDto friendSummaryDto = memberFriendQueryRepository + final SearchFriendSummaryDtoByTag friendSummaryDto = memberFriendQueryRepository .findFriendsByTag(memberId, tag).orElseThrow(FriendNotFoundException::new); return friendSummaryDto.toResponse(); diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java index 38e255fa7..d90e76ef5 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApi.java @@ -7,20 +7,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; +import java.time.ZonedDateTime; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupCreateRequest; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupUpdateRequest; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupsPageResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupsSliceResponse; import site.timecapsulearchive.core.global.common.response.ApiSpec; public interface GroupApi { @@ -133,15 +130,21 @@ ResponseEntity denyGroupInvitation( @ApiResponse( responseCode = "200", description = "ok" + ), + @ApiResponse( + responseCode = "400", + description = "잘못된 파라미터를 받았을 때 발생하는 오류" + ), + @ApiResponse( + responseCode = "403", + description = "그룹에 포함된 사용자가 아닌 경우 발생하는 오류" ) }) - @GetMapping( - value = "/groups/{group_id}", - produces = {"application/json"} - ) - ResponseEntity findGroupById( + ResponseEntity> findGroupDetailById( + Long memberId, + @Parameter(in = ParameterIn.PATH, description = "조회할 그룹 아이디", required = true, schema = @Schema()) - @PathVariable("group_id") Long groupId + Long groupId ); @Operation( @@ -156,16 +159,14 @@ ResponseEntity findGroupById( description = "ok" ) }) - @GetMapping( - value = "/groups", - produces = {"application/json"} - ) - ResponseEntity findGroups( - @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true, schema = @Schema()) - @NotNull @Valid @RequestParam(value = "size") Long size, + ResponseEntity> findGroups( + Long memberId, + + @Parameter(in = ParameterIn.QUERY, description = "페이지 크기", required = true) + int size, - @Parameter(in = ParameterIn.QUERY, description = "마지막 그룹 아이디", required = true, schema = @Schema()) - @NotNull @Valid @RequestParam(value = "group_id") Long groupId + @Parameter(in = ParameterIn.QUERY, description = "마지막 데이터의 시간", required = true) + ZonedDateTime createdAt ); @Operation( diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java index 5dec2042c..e8a067e35 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/api/GroupApiController.java @@ -1,21 +1,29 @@ package site.timecapsulearchive.core.domain.group.api; import jakarta.validation.Valid; +import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupCreateRequest; import site.timecapsulearchive.core.domain.group.data.reqeust.GroupUpdateRequest; import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; -import site.timecapsulearchive.core.domain.group.data.response.GroupsPageResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupsSliceResponse; import site.timecapsulearchive.core.domain.group.service.GroupService; import site.timecapsulearchive.core.global.common.response.ApiSpec; import site.timecapsulearchive.core.global.common.response.SuccessCode; +import site.timecapsulearchive.core.infra.s3.manager.S3PreSignedUrlManager; import site.timecapsulearchive.core.infra.s3.manager.S3UrlGenerator; @@ -26,6 +34,7 @@ public class GroupApiController implements GroupApi { private final GroupService groupService; private final S3UrlGenerator s3UrlGenerator; + private final S3PreSignedUrlManager s3PreSignedUrlManager; @Override public ResponseEntity acceptGroupInvitation(Long groupId, Long memberId) { @@ -66,14 +75,47 @@ public ResponseEntity denyGroupInvitation(Long groupId, Long memberId) { return null; } + @GetMapping( + value = "/{group_id}", + produces = {"application/json"} + ) @Override - public ResponseEntity findGroupById(Long groupId) { - return null; + public ResponseEntity> findGroupDetailById( + @AuthenticationPrincipal final Long memberId, + @PathVariable("group_id") final Long groupId + ) { + final GroupDetailDto groupDetailDto = groupService.findGroupDetailByGroupId(memberId, groupId); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + groupDetailDto.toResponse(s3PreSignedUrlManager::getS3PreSignedUrlForGet) + ) + ); } + @GetMapping( + produces = {"application/json"} + ) @Override - public ResponseEntity findGroups(Long size, Long groupId) { - return null; + public ResponseEntity> findGroups( + @AuthenticationPrincipal final Long memberId, + @RequestParam(defaultValue = "20", value = "size") final int size, + @RequestParam(value = "created_at") final ZonedDateTime createdAt + ) { + final Slice groupsSlice = groupService.findGroupsSlice(memberId, size, + createdAt); + + return ResponseEntity.ok( + ApiSpec.success( + SuccessCode.SUCCESS, + GroupsSliceResponse.createOf( + groupsSlice.getContent(), + groupsSlice.hasNext(), + s3PreSignedUrlManager::getS3PreSignedUrlForGet + ) + ) + ); } @Override diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java new file mode 100644 index 000000000..c45a8a46e --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupDetailDto.java @@ -0,0 +1,31 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.group.data.response.GroupDetailResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberResponse; +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; + +public record GroupDetailDto( + String groupName, + String groupDescription, + String groupProfileUrl, + ZonedDateTime createdAt, + List members +) { + + public GroupDetailResponse toResponse(Function singlePreSignUrlFunction) { + List members = this.members.stream() + .map(GroupMemberDto::toResponse) + .toList(); + + return GroupDetailResponse.builder() + .groupName(groupName) + .groupDescription(groupDescription) + .groupProfileUrl(singlePreSignUrlFunction.apply(groupProfileUrl)) + .createdAt(createdAt) + .members(members) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberDto.java new file mode 100644 index 000000000..09a03c5e1 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberDto.java @@ -0,0 +1,22 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberResponse; + +public record GroupMemberDto( + Long memberId, + String profileUrl, + String nickname, + String tag, + Boolean isOwner +) { + + public GroupMemberResponse toResponse() { + return GroupMemberResponse.builder() + .memberId(memberId) + .profileUrl(profileUrl) + .nickname(nickname) + .tag(tag) + .isOwner(isOwner) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberSummaryDto.java new file mode 100644 index 000000000..d4eb241fe --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupMemberSummaryDto.java @@ -0,0 +1,15 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +import site.timecapsulearchive.core.domain.group.data.response.GroupMemberSummaryResponse; + +public record GroupMemberSummaryDto( + String nickname, + String profileUrl, + Boolean isOpened +) { + + public GroupMemberSummaryResponse toResponse() { + return new GroupMemberSummaryResponse(nickname, profileUrl, isOpened); + } + +} \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupSummaryDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupSummaryDto.java new file mode 100644 index 000000000..a900ff27d --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/dto/GroupSummaryDto.java @@ -0,0 +1,26 @@ +package site.timecapsulearchive.core.domain.group.data.dto; + +import java.time.ZonedDateTime; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.group.data.response.GroupSummaryResponse; + +public record GroupSummaryDto( + Long id, + String groupName, + String groupDescription, + String groupProfileUrl, + ZonedDateTime createdAt, + Boolean isOwner +) { + + public GroupSummaryResponse toResponse(Function preSignedUrlFunction) { + return GroupSummaryResponse.builder() + .id(id) + .name(groupName) + .description(groupDescription) + .profileUrl(preSignedUrlFunction.apply(groupProfileUrl)) + .createdAt(createdAt) + .isOwner(isOwner) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java index f1105e8dc..98b21892d 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupDetailResponse.java @@ -3,24 +3,32 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import java.util.List; +import lombok.Builder; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; +@Builder @Schema(description = "그룹 생성 포맷") public record GroupDetailResponse( @Schema(description = "그룹 이름") - String name, - - @Schema(description = "그룹 생성일") - ZonedDateTime createdDate, + String groupName, @Schema(description = "그룹 프로필 url") - String profileUrl, + String groupProfileUrl, @Schema(description = "그룹 설명") - String description, + String groupDescription, + + @Schema(description = "그룹 생성일") + ZonedDateTime createdAt, @Schema(description = "그룹원 리스트") - List members + List members ) { + public GroupDetailResponse { + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupMemberResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupMemberResponse.java new file mode 100644 index 000000000..db025b957 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupMemberResponse.java @@ -0,0 +1,26 @@ +package site.timecapsulearchive.core.domain.group.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +@Schema(description = "그룹원 정보") +public record GroupMemberResponse( + + @Schema(description = "그룹원 아이디") + Long memberId, + + @Schema(description = "그룹원 프로필 url") + String profileUrl, + + @Schema(description = "그룹원 닉네임") + String nickname, + + @Schema(description = "그룹원 태그") + String tag, + + @Schema(description = "그룹장 여부") + Boolean isOwner +) { + +} \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java index 59e67168b..d43993dce 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupSummaryResponse.java @@ -1,7 +1,11 @@ package site.timecapsulearchive.core.domain.group.data.response; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.ZonedDateTime; +import lombok.Builder; +import site.timecapsulearchive.core.global.common.response.ResponseMappingConstant; +@Builder @Schema(description = "그룹 요약 정보") public record GroupSummaryResponse( @@ -15,7 +19,18 @@ public record GroupSummaryResponse( String profileUrl, @Schema(description = "그룹 설명") - String description + String description, + + @Schema(description = "그룹 생성일") + ZonedDateTime createdAt, + + @Schema(description = "그룹장 여부") + Boolean isOwner ) { + public GroupSummaryResponse { + if (createdAt != null) { + createdAt = createdAt.withZoneSameInstant(ResponseMappingConstant.ZONE_ID); + } + } } \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsPageResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsPageResponse.java deleted file mode 100644 index c9f338b92..000000000 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsPageResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package site.timecapsulearchive.core.domain.group.data.response; - -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -@Schema(description = "그룹 리스트 페이징") -public record GroupsPageResponse( - - @Schema(description = "그룹 리스트") - List groups, - - @Schema(description = "다음 페이지 유무") - - Boolean hasNext, - - @Schema(description = "이전 페이지 유무") - - Boolean hasPrevious -) { - -} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsSliceResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsSliceResponse.java new file mode 100644 index 000000000..d0f4ad450 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/data/response/GroupsSliceResponse.java @@ -0,0 +1,29 @@ +package site.timecapsulearchive.core.domain.group.data.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.function.Function; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; + +@Schema(description = "사용자의 그룹 목록 응답") +public record GroupsSliceResponse( + + @Schema(description = "그룹 목록") + List groups, + + @Schema(description = "다음 페이지 유무") + Boolean hasNext +) { + + public static GroupsSliceResponse createOf( + List groups, + Boolean hasNext, + Function preSignedUrlFunction + ) { + List responses = groups.stream() + .map(dto -> dto.toResponse(preSignedUrlFunction)) + .toList(); + + return new GroupsSliceResponse(responses, hasNext); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java new file mode 100644 index 000000000..9b7e5a201 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/repository/GroupQueryRepository.java @@ -0,0 +1,91 @@ +package site.timecapsulearchive.core.domain.group.repository; + +import static com.querydsl.core.group.GroupBy.groupBy; +import static com.querydsl.core.group.GroupBy.list; +import static site.timecapsulearchive.core.domain.group.entity.QGroup.group; +import static site.timecapsulearchive.core.domain.group.entity.QMemberGroup.memberGroup; +import static site.timecapsulearchive.core.domain.member.entity.QMember.member; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; + +@Repository +@RequiredArgsConstructor +public class GroupQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + final List groups = jpaQueryFactory + .select( + Projections.constructor( + GroupSummaryDto.class, + group.id, + group.groupName, + group.groupDescription, + group.groupProfileUrl, + group.createdAt, + memberGroup.isOwner + ) + ) + .from(memberGroup) + .join(memberGroup.group, group) + .where(memberGroup.member.id.eq(memberId).and(memberGroup.createdAt.lt(createdAt))) + .limit(size + 1) + .fetch(); + + final boolean hasNext = groups.size() > size; + if (hasNext) { + groups.remove(size); + } + + return new SliceImpl<>(groups, Pageable.ofSize(size), groups.size() > size); + } + + public Optional findGroupDetailByGroupId(final Long groupId) { + return Optional.ofNullable( + jpaQueryFactory + .selectFrom(group) + .join(memberGroup).on(memberGroup.group.id.eq(group.id)) + .join(member).on(member.id.eq(memberGroup.member.id)) + .where(group.id.eq(groupId)) + .transform( + groupBy(group.id).as( + Projections.constructor( + GroupDetailDto.class, + group.groupName, + group.groupDescription, + group.groupProfileUrl, + group.createdAt, + list( + Projections.constructor( + GroupMemberDto.class, + member.id, + member.profileUrl, + member.nickname, + member.tag, + memberGroup.isOwner + ) + ) + ) + ) + ) + .get(groupId) + ); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java index 58ca64206..98b3b1603 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/group/service/GroupService.java @@ -1,15 +1,21 @@ package site.timecapsulearchive.core.domain.group.service; import lombok.RequiredArgsConstructor; +import java.time.ZonedDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import site.timecapsulearchive.core.domain.group.data.dto.GroupCreateDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; import site.timecapsulearchive.core.domain.group.entity.Group; import site.timecapsulearchive.core.domain.group.entity.MemberGroup; import site.timecapsulearchive.core.domain.group.exception.GroupNotFoundException; +import site.timecapsulearchive.core.domain.group.repository.GroupQueryRepository; import site.timecapsulearchive.core.domain.group.repository.GroupRepository; import site.timecapsulearchive.core.domain.group.repository.MemberGroupRepository; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -26,6 +32,7 @@ public class GroupService { private final MemberGroupRepository memberGroupRepository; private final TransactionTemplate transactionTemplate; private final SocialNotificationManager socialNotificationManager; + private final GroupQueryRepository groupQueryRepository; public void createGroup(final Long memberId, final GroupCreateDto dto) { final Member member = memberRepository.findMemberById(memberId) @@ -52,4 +59,29 @@ public Group findGroupById(Long groupId) { return groupRepository.findGroupById(groupId) .orElseThrow(GroupNotFoundException::new); } + + @Transactional(readOnly = true) + public Slice findGroupsSlice( + final Long memberId, + final int size, + final ZonedDateTime createdAt + ) { + return groupQueryRepository.findGroupsSlice(memberId, size, createdAt); + } + + @Transactional(readOnly = true) + public GroupDetailDto findGroupDetailByGroupId(final Long memberId, final Long groupId) { + final GroupDetailDto groupDetailDto = groupQueryRepository.findGroupDetailByGroupId(groupId) + .orElseThrow(GroupNotFoundException::new); + + final boolean isGroupMember = groupDetailDto.members() + .stream() + .anyMatch(m -> m.memberId().equals(memberId)); + + if (!isGroupMember) { + throw new GroupNotFoundException(); + } + + return groupDetailDto; + } } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/dto/SignUpRequestDto.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/dto/SignUpRequestDto.java index 403b998f4..4ec2aa7c3 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/dto/SignUpRequestDto.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/dto/SignUpRequestDto.java @@ -1,6 +1,9 @@ package site.timecapsulearchive.core.domain.member.data.dto; +import com.aventrix.jnanoid.jnanoid.NanoIdUtils; +import site.timecapsulearchive.core.domain.member.entity.MemberTemporary; import site.timecapsulearchive.core.domain.member.entity.SocialType; +import site.timecapsulearchive.core.global.util.nickname.MakeRandomNickNameUtil; public record SignUpRequestDto( String authId, @@ -9,4 +12,15 @@ public record SignUpRequestDto( SocialType socialType ) { + public MemberTemporary toMemberTemporary() { + return MemberTemporary.builder() + .authId(authId) + .nickname(MakeRandomNickNameUtil.makeRandomNickName()) + .email(email) + .profileUrl(profileUrl) + .socialType(socialType) + .tag(NanoIdUtils.randomNanoId()) + .build(); + } + } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/mapper/MemberMapper.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/mapper/MemberMapper.java index 5090a9755..d25037621 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/mapper/MemberMapper.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/mapper/MemberMapper.java @@ -6,9 +6,7 @@ import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import site.timecapsulearchive.core.domain.auth.data.request.SignUpRequest; import site.timecapsulearchive.core.domain.member.data.dto.MemberNotificationDto; -import site.timecapsulearchive.core.domain.member.data.dto.SignUpRequestDto; import site.timecapsulearchive.core.domain.member.data.response.MemberNotificationResponse; import site.timecapsulearchive.core.domain.member.data.response.MemberNotificationSliceResponse; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -39,26 +37,6 @@ public Member OAuthToEntity( .build(); } - public SignUpRequestDto signUpRequestToDto(final SignUpRequest request) { - return new SignUpRequestDto( - request.authId(), - request.email(), - request.profileUrl(), - request.socialType() - ); - } - - public Member signUpRequestDtoToEntity(final SignUpRequestDto dto) { - return Member.builder() - .authId(dto.authId()) - .nickname(MakeRandomNickNameUtil.makeRandomNickName()) - .email(dto.email()) - .profileUrl(dto.profileUrl()) - .socialType(dto.socialType()) - .tag(NanoIdUtils.randomNanoId()) - .build(); - } - public MemberNotificationSliceResponse notificationSliceToResponse( final List content, final boolean hasNext diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java index 93287c46e..2197b6b78 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/data/response/MemberNotificationResponse.java @@ -1,17 +1,31 @@ package site.timecapsulearchive.core.domain.member.data.response; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.ZonedDateTime; import lombok.Builder; import site.timecapsulearchive.core.domain.member.entity.CategoryName; import site.timecapsulearchive.core.domain.member.entity.NotificationStatus; +@Schema(description = "알림 정보") @Builder public record MemberNotificationResponse( + + @Schema(description = "알림 제목") String title, + + @Schema(description = "알림 내용") String text, + + @Schema(description = "알림 생성일") ZonedDateTime createdAt, + + @Schema(description = "알림 이미지 링크") String imageUrl, + + @Schema(description = "알림 카테고리 이름") CategoryName categoryName, + + @Schema(description = "알림 상태") NotificationStatus status ) { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java index caa93a754..730084363 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/Member.java @@ -23,6 +23,7 @@ import site.timecapsulearchive.core.domain.group.entity.MemberGroup; import site.timecapsulearchive.core.domain.history.entity.History; import site.timecapsulearchive.core.global.entity.BaseEntity; +import site.timecapsulearchive.core.global.util.NullCheck; @Entity @Getter @@ -96,27 +97,18 @@ public class Member extends BaseEntity { @Builder private Member(String profileUrl, String nickname, SocialType socialType, String email, - String authId, String password, String tag) { - this.profileUrl = profileUrl; - this.nickname = nickname; - this.socialType = socialType; - this.email = email; - this.isVerified = false; + String authId, String password, String tag, byte[] phone, byte[] phone_hash) { + this.profileUrl = NullCheck.validate(profileUrl, "Entity: profile"); + this.nickname = NullCheck.validate(nickname, "Entity: nickname"); + this.socialType = NullCheck.validate(socialType, "Entity: socialType"); + this.email = NullCheck.validate(email, "Entity: email"); + this.tag = NullCheck.validate(tag, "Entity: tag"); + this.authId = NullCheck.validate(authId, "Entity: authId"); + this.isVerified = true; this.notificationEnabled = false; - this.authId = authId; this.password = password; - this.tag = tag; - } - - public void updateVerification() { - this.isVerified = true; - } - - public void updatePhoneNumber(byte[] phone) { this.phone = phone; - } - - public void updatePhoneHash(byte[] phone_hash) { this.phone_hash = phone_hash; } + } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/MemberTemporary.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/MemberTemporary.java new file mode 100644 index 000000000..7ecb0e197 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/entity/MemberTemporary.java @@ -0,0 +1,76 @@ +package site.timecapsulearchive.core.domain.member.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Email; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.timecapsulearchive.core.global.entity.BaseEntity; +import site.timecapsulearchive.core.global.util.NullCheck; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "MEMBER_TEMPORARY") +public class MemberTemporary extends BaseEntity { + + @Id + @Column(name = "member_temporary_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "profile_url", nullable = false) + private String profileUrl; + + @Column(name = "nickname", nullable = false) + private String nickname; + + @Column(name = "social_type", nullable = false) + @Enumerated(EnumType.STRING) + private SocialType socialType; + + @Email + @Column(name = "email", nullable = false) + private String email; + @Column(name = "is_verified", nullable = false) + private Boolean isVerified; + + @Column(name = "auth_id", nullable = false, unique = true) + private String authId; + + @Column(name = "tag", nullable = false, unique = true) + private String tag; + + @Builder + public MemberTemporary(String profileUrl, String nickname, SocialType socialType, String email, + String authId, String tag) { + this.profileUrl = NullCheck.validate(profileUrl, "Entity: profile"); + this.nickname = NullCheck.validate(nickname, "Entity: nickname"); + this.socialType = NullCheck.validate(socialType, "Entity: socialType"); + this.email = NullCheck.validate(email, "Entity: email"); + this.isVerified = false; + this.authId = NullCheck.validate(authId, "Entity: authId"); + this.tag = NullCheck.validate(tag, "Entity: tag"); + } + + public Member toMember(final byte[] phone_hash, final byte[] phone) { + return Member.builder() + .profileUrl(profileUrl) + .nickname(nickname) + .socialType(socialType) + .email(email) + .authId(authId) + .tag(tag) + .phone_hash(phone_hash) + .phone(phone) + .build(); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberTemporaryRepository.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberTemporaryRepository.java new file mode 100644 index 000000000..b5905de97 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/repository/MemberTemporaryRepository.java @@ -0,0 +1,14 @@ +package site.timecapsulearchive.core.domain.member.repository; + +import java.util.Optional; +import org.springframework.data.repository.Repository; +import site.timecapsulearchive.core.domain.member.entity.MemberTemporary; + +public interface MemberTemporaryRepository extends Repository { + + MemberTemporary save(MemberTemporary memberTemporary); + + Optional findById(Long id); + + void delete(MemberTemporary memberTemporary); +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/service/MemberService.java b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/service/MemberService.java index 3955c9137..494c3f0f1 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/service/MemberService.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/domain/member/service/MemberService.java @@ -19,6 +19,7 @@ import site.timecapsulearchive.core.domain.member.data.response.MemberNotificationStatusResponse; import site.timecapsulearchive.core.domain.member.data.response.MemberStatusResponse; import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.domain.member.entity.MemberTemporary; import site.timecapsulearchive.core.domain.member.entity.SocialType; import site.timecapsulearchive.core.domain.member.exception.AlreadyVerifiedException; import site.timecapsulearchive.core.domain.member.exception.CredentialsNotMatchedException; @@ -26,6 +27,7 @@ import site.timecapsulearchive.core.domain.member.exception.NotVerifiedMemberException; import site.timecapsulearchive.core.domain.member.repository.MemberQueryRepository; import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.domain.member.repository.MemberTemporaryRepository; @Slf4j @Service @@ -34,6 +36,7 @@ public class MemberService { private final MemberRepository memberRepository; + private final MemberTemporaryRepository memberTemporaryRepository; private final MemberQueryRepository memberQueryRepository; private final PasswordEncoder passwordEncoder; @@ -41,9 +44,9 @@ public class MemberService { @Transactional public Long createMember(final SignUpRequestDto dto) { - final Member member = memberMapper.signUpRequestDtoToEntity(dto); + final MemberTemporary member = dto.toMemberTemporary(); - final Member savedMember = memberRepository.save(member); + final MemberTemporary savedMember = memberTemporaryRepository.save(member); return savedMember.getId(); } diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java index 1636b5940..cf0b91e0f 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/rabbitmq/RabbitmqComponentConstants.java @@ -7,13 +7,19 @@ public enum RabbitmqComponentConstants { CAPSULE_SKIN_QUEUE("request.createCapsuleSkin.queue", "fail.request.createCapsuleSkin.queue"), - CAPSULE_SKIN_EXCHANGE("request.createCapsuleSkin.exchange", "fail.request.createCapsuleSkin.exchange"), - FRIEND_REQUEST_NOTIFICATION_QUEUE("notification.friendRequest.queue", "fail.notification.friendRequest.queue"), - FRIEND_REQUEST_NOTIFICATION_EXCHANGE("notification.friendRequest.exchange", "fail.notification.friendRequest.exchange"), - FRIEND_ACCEPT_NOTIFICATION_QUEUE("notification.friendAccept.queue", "fail.notification.friendAccept.queue"), - FRIEND_ACCEPT_NOTIFICATION_EXCHANGE("notification.friendAccept.exchange", "fail.notification.friendAccept.exchange"), + CAPSULE_SKIN_EXCHANGE("request.createCapsuleSkin.exchange", + "fail.request.createCapsuleSkin.exchange"), + FRIEND_REQUEST_NOTIFICATION_QUEUE("notification.friendRequest.queue", + "fail.notification.friendRequest.queue"), + FRIEND_REQUEST_NOTIFICATION_EXCHANGE("notification.friendRequest.exchange", + "fail.notification.friendRequest.exchange"), + FRIEND_ACCEPT_NOTIFICATION_QUEUE("notification.friendAccept.queue", + "fail.notification.friendAccept.queue"), + FRIEND_ACCEPT_NOTIFICATION_EXCHANGE("notification.friendAccept.exchange", + "fail.notification.friendAccept.exchange"), GROUP_INVITE_QUEUE("notification.groupInvite.queue", "fail.notification.groupInvite.queue"), - GROUP_INVITE_EXCHANGE("notification.groupInvite.exchange", "fail.notification.groupInvite.exchange"); + GROUP_INVITE_EXCHANGE("notification.groupInvite.exchange", + "fail.notification.groupInvite.exchange"); private final String successComponent; private final String failComponent; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java index 7a815d08a..d1f5dffa2 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/ErrorCode.java @@ -55,14 +55,15 @@ public enum ErrorCode { //friend FRIEND_NOT_FOUND_ERROR(404, "FRIEND-001", "친구를 찾지 못하였습니다"), - FRIEND_DUPLICATE_ID_ERROR(404, "FRIEND-001", "친구 아이디가 중복되었습니다."), + FRIEND_DUPLICATE_ID_ERROR(404, "FRIEND-002", "친구 아이디가 중복되었습니다."), //group GROUP_CREATE_ERROR(400, "GROUP-001", "그룹 생성에 실패하였습니다."), GROUP_NOT_FOUND_ERROR(404, "GROUP-002", "그룹을 찾을 수 없습니다"), //friend invite - FRIEND_INVITE_NOT_FOUND_ERROR(404, "FRIEND-INVITE", "친구 요청을 찾지 못하였습니다."); + FRIEND_INVITE_NOT_FOUND_ERROR(404, "FRIEND-INVITE-001", "친구 요청을 찾지 못하였습니다."), + FRIEND_TWO_WAY_INVITE_ERROR(400, "FRIEND-INVITE-002", "친구 요청을 받은 상태입니다."); private final int status; private final String code; 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 d7435216d..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; @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import site.timecapsulearchive.core.global.error.exception.BusinessException; +import site.timecapsulearchive.core.global.error.exception.NullCheckValidateException; import site.timecapsulearchive.core.infra.sms.exception.ExternalApiException; @RestControllerAdvice @@ -67,6 +69,18 @@ protected ResponseEntity handleRequestTypeNotValidException( .body(response); } + @ExceptionHandler(NullCheckValidateException.class) + protected ResponseEntity handleNullCheckValidException( + NullCheckValidateException e + ) { + log.warn(e.getMessage(), e); + + final ErrorResponse response = ErrorResponse.fromParameter( + REQUEST_PARAMETER_NOT_FOUND_ERROR, e.getMessage()); + return ResponseEntity.status(REQUEST_PARAMETER_NOT_FOUND_ERROR.getStatus()) + .body(response); + } + @ExceptionHandler(ExternalApiException.class) protected ResponseEntity handleExternalApiException(ExternalApiException e) { log.warn(e.getMessage(), e); @@ -131,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/main/java/site/timecapsulearchive/core/global/error/exception/NullCheckValidateException.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/NullCheckValidateException.java new file mode 100644 index 000000000..f139c6d30 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/NullCheckValidateException.java @@ -0,0 +1,8 @@ +package site.timecapsulearchive.core.global.error.exception; + +public class NullCheckValidateException extends RuntimeException { + + public NullCheckValidateException(String missingField) { + super(missingField); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/util/NullCheck.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/util/NullCheck.java new file mode 100644 index 000000000..a6f2b2bf4 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/util/NullCheck.java @@ -0,0 +1,19 @@ +package site.timecapsulearchive.core.global.util; + +import java.util.Objects; +import java.util.Optional; +import site.timecapsulearchive.core.global.error.exception.NullCheckValidateException; + +public final class NullCheck { + + private NullCheck() { + } + + public static T validate(T value, String message) { + try { + return Optional.of(Objects.requireNonNull(value, message)).get(); + } catch (NullPointerException e) { + throw new NullCheckValidateException(e.getMessage()); + } + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/manager/S3PreSignedUrlManager.java b/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/manager/S3PreSignedUrlManager.java index 0746b4d14..c6e54d65c 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/manager/S3PreSignedUrlManager.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/infra/s3/manager/S3PreSignedUrlManager.java @@ -157,7 +157,7 @@ public String getS3PreSignedUrlForGet(final String fileName) { * @return 구분자 ,로 구분된 각각의 서명된 이미지 url */ public List getS3PreSignedUrlsForGet(final String fileNames) { - if (fileNames == null) { + if (fileNames == null || fileNames.isEmpty()) { return Collections.emptyList(); } diff --git a/backend/core/src/main/resources/db/migration/V23__group_capsule_open_update.sql b/backend/core/src/main/resources/db/migration/V23__group_capsule_open_update.sql new file mode 100644 index 000000000..af0c97cd1 --- /dev/null +++ b/backend/core/src/main/resources/db/migration/V23__group_capsule_open_update.sql @@ -0,0 +1,2 @@ +-- update group_capsule_open Table +ALTER TABLE group_capsule_open ADD COLUMN is_opened boolean; \ No newline at end of file diff --git a/backend/core/src/main/resources/db/migration/V24__member_temporary_tabale_create.sql b/backend/core/src/main/resources/db/migration/V24__member_temporary_tabale_create.sql new file mode 100644 index 000000000..31382333d --- /dev/null +++ b/backend/core/src/main/resources/db/migration/V24__member_temporary_tabale_create.sql @@ -0,0 +1,15 @@ +CREATE TABLE member_temporary +( + member_temporary_id INT AUTO_INCREMENT PRIMARY KEY, + profile_url VARCHAR(255), + nickname VARCHAR(255), + social_type VARCHAR(255), + email VARCHAR(255), + is_verified BOOLEAN, + auth_id VARCHAR(255), + tag VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +alter table friend_invite drop column friend_status; \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java index d9bfee830..c0430087e 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/RepositoryTest.java @@ -8,8 +8,9 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import site.timecapsulearchive.core.global.config.JpaAuditingConfig; +import site.timecapsulearchive.core.global.config.QueryDSLConfig; -@Import(JpaAuditingConfig.class) +@Import(value = {JpaAuditingConfig.class, QueryDSLConfig.class}) @DataJpaTest @FlywayTestExtension @FlywayTest diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberFixture.java deleted file mode 100644 index 3aa61ebb2..000000000 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberFixture.java +++ /dev/null @@ -1,70 +0,0 @@ -package site.timecapsulearchive.core.common.fixture; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import site.timecapsulearchive.core.common.dependency.UnitTestDependency; -import site.timecapsulearchive.core.domain.member.entity.Member; -import site.timecapsulearchive.core.domain.member.entity.SocialType; -import site.timecapsulearchive.core.global.common.wrapper.ByteArrayWrapper; -import site.timecapsulearchive.core.global.security.encryption.HashEncryptionManager; - -public class MemberFixture { - - private static final HashEncryptionManager hashEncryptionManager = UnitTestDependency.hashEncryptionManager(); - - public static Member member(int dataPrefix) { - Member member = Member.builder() - .socialType(SocialType.GOOGLE) - .nickname(dataPrefix + "testNickname") - .email(dataPrefix + "test@google.com") - .authId(dataPrefix + "test") - .profileUrl(dataPrefix + "test.com") - .tag(dataPrefix + "testTag") - .build(); - - byte[] number = getPhoneBytes(dataPrefix); - member.updatePhoneHash(hashEncryptionManager.encrypt(number)); - - return member; - } - - public static byte[] getPhoneBytes(int dataPrefix) { - return ("0" + (1000000000 + dataPrefix)).getBytes(StandardCharsets.UTF_8); - } - - public static List getPhones() { - return List.of( - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341234")), - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341235")), - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341236")), - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341237")), - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341238")), - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341239")), - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341240")), - new ByteArrayWrapper(MemberFixture.getPhoneBytes("01012341241")) - ); - } - - private static byte[] getPhoneBytes(String phone) { - return hashEncryptionManager.encrypt(phone.getBytes(StandardCharsets.UTF_8)); - } - - public static List members(int start, int count) { - List result = new ArrayList<>(); - for (int index = start; index < start + count; index++) { - result.add(member(index)); - } - - return result; - } - - public static List getPhoneBytesList(int start, int count) { - List result = new ArrayList<>(); - for (int index = start; index < start + count; index++) { - result.add(getPhoneBytes(index)); - } - - return result; - } -} \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/CapsuleFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleFixture.java similarity index 58% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/CapsuleFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleFixture.java index fc1398129..b691ddd19 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/CapsuleFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import java.time.ZonedDateTime; import java.util.List; @@ -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/CapsuleSkinFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleSkinFixture.java similarity index 91% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/CapsuleSkinFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleSkinFixture.java index 260202078..df32df1c6 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/CapsuleSkinFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/CapsuleSkinFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; import site.timecapsulearchive.core.domain.capsuleskin.entity.Motion; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/FriendInviteFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/FriendInviteFixture.java similarity index 83% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/FriendInviteFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/FriendInviteFixture.java index 6a2ab7ae9..65cda05dd 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/FriendInviteFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/FriendInviteFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.member.entity.Member; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupCapsuleOpenFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupCapsuleOpenFixture.java new file mode 100644 index 000000000..7ee3e0029 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupCapsuleOpenFixture.java @@ -0,0 +1,21 @@ +package site.timecapsulearchive.core.common.fixture.domain; + +import java.util.List; +import site.timecapsulearchive.core.domain.capsule.entity.Capsule; +import site.timecapsulearchive.core.domain.capsule.entity.GroupCapsuleOpen; +import site.timecapsulearchive.core.domain.member.entity.Member; + +public class GroupCapsuleOpenFixture { + + public static List groupCapsuleOpens(Boolean isOpened, Capsule capsule, + List groupMembers) { + return groupMembers.stream() + .map(member -> GroupCapsuleOpen.builder() + .isOpened(isOpened) + .capsule(capsule) + .member(member) + .build() + ).toList(); + } + +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupFixture.java new file mode 100644 index 000000000..615613e58 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/GroupFixture.java @@ -0,0 +1,19 @@ +package site.timecapsulearchive.core.common.fixture.domain; + +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") + .groupProfileUrl("test_group") + .build(); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/ImageFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/ImageFixture.java similarity index 92% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/ImageFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/ImageFixture.java index cf9fcf86a..0c6122095 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/ImageFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/ImageFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import java.util.List; import java.util.stream.IntStream; 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 new file mode 100644 index 000000000..9126a5f9a --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFixture.java @@ -0,0 +1,80 @@ +package site.timecapsulearchive.core.common.fixture.domain; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import site.timecapsulearchive.core.common.dependency.UnitTestDependency; +import site.timecapsulearchive.core.domain.member.entity.Member; +import site.timecapsulearchive.core.domain.member.entity.SocialType; +import site.timecapsulearchive.core.global.common.wrapper.ByteArrayWrapper; +import site.timecapsulearchive.core.global.security.encryption.HashEncryptionManager; + +public class MemberFixture { + + private static final HashEncryptionManager hashEncryptionManager = UnitTestDependency.hashEncryptionManager(); + + /** + * 테스트 픽스처 - 멤버 마다 상이한 값을 위한 dataPrefix를 주면 멤버를 생성한다. + *
주의 - 테스트에서 같은 prefix를 사용하면 오류가 발생하므로 서로 다른 prefix를 쓰도록 해야함. + * @param dataPrefix prefix + * @return {@code Member} 테스트 픽스처 + */ + public static Member member(int dataPrefix) { + byte[] number = getPhoneBytes(dataPrefix); + + Member member = Member.builder() + .socialType(SocialType.GOOGLE) + .nickname(dataPrefix + "testNickname") + .email(dataPrefix + "test@google.com") + .authId(dataPrefix + "test") + .profileUrl(dataPrefix + "test.com") + .tag(dataPrefix + "testTag") + .phone_hash(hashEncryptionManager.encrypt(number)) + .build(); + + return member; + } + + /** + * 테스트 픽스처 - 멤버마다 상이한 번호를 위해 dataPrefix를 주면 해당 dataPrefix에 대한 핸드폰 번호 바이트를 반환한다. + *
주의 - 테스트에서 같은 prefix를 사용하면 오류가 발생하므로 서로 다른 prefix를 쓰도록 해야함. + * + * @param dataPrefix prefix + * @return 핸드폰 번호 바이트 + */ + public static byte[] getPhoneBytes(int dataPrefix) { + return ("0" + (1000000000 + dataPrefix)).getBytes(StandardCharsets.UTF_8); + } + + public static List getPhones(int count) { + return IntStream.range(0, count) + .mapToObj(i -> new ByteArrayWrapper(MemberFixture.getPhoneBytes(count))) + .toList(); + } + + /** + * 테스트 픽스처 - 크기와 멤버 마다 상이한 값을 위한 startDataPrefix를 주면 멤버들을 생성한다. + *
주의 - 테스트에서 같은 prefix를 사용하면 오류가 발생하므로 서로 다른 prefix를 쓰도록 해야함. + * @param startDataPrefix 시작 prefix + * @param count 크기 + * @return {@code List} 테스트 픽스처들 + */ + public static List members(int startDataPrefix, int count) { + List result = new ArrayList<>(); + for (int index = startDataPrefix; index < startDataPrefix + count; index++) { + result.add(member(index)); + } + + return result; + } + + public static List getPhoneBytesList(int start, int count) { + List result = new ArrayList<>(); + for (int index = start; index < start + count; index++) { + result.add(getPhoneBytes(index)); + } + + return result; + } +} \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberFriendFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFriendFixture.java similarity index 86% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberFriendFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFriendFixture.java index 130dbc9eb..4c504a1ba 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/MemberFriendFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberFriendFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; import site.timecapsulearchive.core.domain.member.entity.Member; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java new file mode 100644 index 000000000..6ec83101e --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/MemberGroupFixture.java @@ -0,0 +1,64 @@ +package site.timecapsulearchive.core.common.fixture.domain; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.List; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.member.entity.Member; + +public class MemberGroupFixture { + + /** + * 테스트 픽스처로 그룹장으로 그룹 멤버를 생성한다 + * + * @param member 그룹장이 될 멤버 + * @param group 대상 그룹 + * @return 그룹 멤버 테스트 픽스처 + */ + public static MemberGroup memberGroup(Member member, Group group) { + return MemberGroup.createGroupOwner(member, group); + } + + /** + * 테스트 픽스처 - 사용자, 그룹, 그룹장 여부를 받아 그룹원을 만들어준다. + * @param member 사용자 + * @param group 그룹 + * @param isOwner 그룹장 여부 + * @return MemberGroup 테스트 픽스처 + */ + public static MemberGroup memberGroup(Member member, Group group, Boolean isOwner) { + try { + Constructor declaredConstructor = MemberGroup.class.getDeclaredConstructor(); + declaredConstructor.setAccessible(true); + + MemberGroup memberGroup = declaredConstructor.newInstance(); + setFieldValue(memberGroup, "member", member); + setFieldValue(memberGroup, "group", group); + setFieldValue(memberGroup, "isOwner", isOwner); + + return memberGroup; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setFieldValue(Object instance, String fieldName, Object value) + throws NoSuchFieldException, IllegalAccessException { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, value); + } + + /** + * 테스트 픽스처 - 그룹원들과 그룹을 주면 그룹원들 목록을 만들어준다(그룹장 X) + * @param members 그룹원들 목록 + * @param group 그룹 + * @return {@code List} 테스트 픽스처들 + */ + public static List memberGroups(List members, Group group) { + return members.stream() + .map(m -> memberGroup(m, group, Boolean.FALSE)) + .toList(); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/NotificationCategoryFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/NotificationCategoryFixture.java similarity index 95% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/NotificationCategoryFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/NotificationCategoryFixture.java index edca0a70a..231468248 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/NotificationCategoryFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/NotificationCategoryFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import java.lang.reflect.Constructor; import java.lang.reflect.Field; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/NotificationFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/NotificationFixture.java similarity index 96% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/NotificationFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/NotificationFixture.java index 2cc40669b..b97508486 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/NotificationFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/NotificationFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import java.lang.reflect.Constructor; import java.lang.reflect.Field; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/VideoFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/VideoFixture.java similarity index 92% rename from backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/VideoFixture.java rename to backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/VideoFixture.java index 71b1f2556..4ca0f1aa0 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/VideoFixture.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/domain/VideoFixture.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.common.fixture; +package site.timecapsulearchive.core.common.fixture.domain; import java.util.List; import java.util.stream.IntStream; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/CapsuleDtoFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/CapsuleDtoFixture.java new file mode 100644 index 000000000..ac624f9c4 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/CapsuleDtoFixture.java @@ -0,0 +1,36 @@ +package site.timecapsulearchive.core.common.fixture.dto; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; +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.group_capsule.data.dto.GroupCapsuleDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberSummaryDto; + +public class CapsuleDtoFixture { + + private static ZonedDateTime now = ZonedDateTime.now(); + + public static Optional getCapsuleDetailDto(Long capsuleId, Boolean isOpened, + ZonedDateTime dueDate) { + return Optional.of( + new CapsuleDetailDto(capsuleId, "test", dueDate, "testNickname", "testUrl", now, + null, "testAddress", "testRoadName", "testTitle", "testContent", "testImages", + "testVideos", isOpened, CapsuleType.PUBLIC) + ); + } + + public static Optional getGroupCapsuleDetailDto(Long capsuleId, + boolean isOpened, ZonedDateTime now, int count) { + return Optional.of(new GroupCapsuleDetailDto(getCapsuleDetailDto(capsuleId, isOpened, now).get(), + getGroupMemberSummaryDtos(count))); + } + + private static List getGroupMemberSummaryDtos(int count) { + return IntStream.range(0, count) + .mapToObj(i -> new GroupMemberSummaryDto(i + "testNickname", i + "testUrl", true)) + .toList(); + } +} \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendDtoFixture.java b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendDtoFixture.java new file mode 100644 index 000000000..5cf59d72a --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/common/fixture/dto/FriendDtoFixture.java @@ -0,0 +1,38 @@ +package site.timecapsulearchive.core.common.fixture.dto; + +import java.util.List; +import java.util.Optional; +import java.util.stream.LongStream; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; +import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; +import site.timecapsulearchive.core.global.common.wrapper.ByteArrayWrapper; + +public class FriendDtoFixture { + + public static List getFriendSummaryDtos(int count) { + return LongStream.range(0, count) + .mapToObj(i -> SearchFriendSummaryDto.builder() + .id(i) + .profileUrl(i + "testProfile.com") + .nickname(i + "testNickname") + .phoneHash(new ByteArrayWrapper(MemberFixture.getPhoneBytes((int) i))) + .isFriend(true) + .isFriendInviteToFriend(false) + .isFriendInviteToMe(false) + .build()) + .toList(); + } + + public static Optional getFriendSummaryDtoByTag() { + return Optional.of(SearchFriendSummaryDtoByTag.builder() + .id(1L) + .profileUrl("testProfile.com") + .nickname("testNickname") + .isFriend(true) + .isFriendInviteToFriend(false) + .isFriendInviteToMe(false) + .build()); + + } +} 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..f3d6a84fd --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/generic_capsule/repository/CapsuleQueryRepositoryTest.java @@ -0,0 +1,289 @@ +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).isNotEmpty(); + 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).isNotEmpty(); + 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).isNotEmpty(); + 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).isNotEmpty(); + 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).isNotEmpty(); + 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).isNotEmpty(); + 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).isNotEmpty(); + 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( + friendIds, mbr + ); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(capsules).isNotEmpty(); + 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..7965e63a6 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/repository/GroupCapsuleQueryRepositoryTest.java @@ -0,0 +1,201 @@ +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 java.util.List; +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; +import org.springframework.data.domain.Slice; +import org.springframework.test.context.TestConstructor; +import org.springframework.test.context.TestConstructor.AutowireMode; +import org.springframework.transaction.annotation.Transactional; +import site.timecapsulearchive.core.common.RepositoryTest; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleFixture; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleSkinFixture; +import site.timecapsulearchive.core.common.fixture.domain.GroupCapsuleOpenFixture; +import site.timecapsulearchive.core.common.fixture.domain.GroupFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberGroupFixture; +import site.timecapsulearchive.core.domain.capsule.entity.Capsule; +import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.entity.GroupCapsuleOpen; +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.capsuleskin.entity.CapsuleSkin; +import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberSummaryDto; +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 groupLeaderId; + private Long capsuleId; + private Capsule capsule; + + GroupCapsuleQueryRepositoryTest(JPAQueryFactory jpaQueryFactory) { + this.groupCapsuleQueryRepository = new GroupCapsuleQueryRepository(jpaQueryFactory); + } + + @BeforeEach + @Transactional + void setup(@Autowired EntityManager entityManager) { + // 그룹원 + List groupMember = MemberFixture.members(1, 5); + groupMember.forEach(entityManager::persist); + Member owner = groupMember.get(0); + groupLeaderId = owner.getId(); + + //캡슐 스킨 + CapsuleSkin capsuleSkin = CapsuleSkinFixture.capsuleSkin(owner); + entityManager.persist(capsuleSkin); + + //그룹 + Group group = GroupFixture.group(); + entityManager.persist(group); + + //그룹 구성 + List memberGroups = MemberGroupFixture.memberGroups(groupMember, group); + memberGroups.forEach(entityManager::persist); + + //그룹 캡슐 + capsule = CapsuleFixture.groupCapsule(owner, capsuleSkin, group); + entityManager.persist(capsule); + capsuleId = capsule.getId(); + + //그룹 캡슐 오픈 여부 + List groupCapsuleOpens = GroupCapsuleOpenFixture.groupCapsuleOpens(false, + capsule, groupMember); + groupCapsuleOpens.forEach(entityManager::persist); + } + + @Test + void 그룹캡슐_아이디로_그룹_캡슐의_상세_조회_하면_상세_내용을_조회할_수_있다() { + // given + //when + GroupCapsuleDetailDto groupCapsuleDetailDto = groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId( + capsuleId).orElseThrow(); + CapsuleDetailDto capsuleDetailDto = groupCapsuleDetailDto.capsuleDetailDto(); + + //then + SoftAssertions.assertSoftly( + softly -> { + softly.assertThat(capsuleDetailDto).isNotNull(); + softly.assertThat(capsuleDetailDto.capsuleType()).isEqualTo(capsule.getType()); + softly.assertThat(capsuleDetailDto.title()).isEqualTo(capsule.getTitle()); + softly.assertThat(capsuleDetailDto.content()).isEqualTo(capsule.getContent()); + softly.assertThat(capsuleDetailDto.capsuleId()).isEqualTo(capsule.getId()); + } + ); + } + + @Test + void 그룹캡슐_아이디로_그룹_캡슐의_상세_조회_하면_그룹원_정보를_조회할_수_있다() { + //given + //when + GroupCapsuleDetailDto detailDto = groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId( + capsuleId).orElseThrow(); + List summaryDto = detailDto.members(); + + //then + SoftAssertions.assertSoftly( + softly -> { + softly.assertThat(summaryDto).isNotEmpty(); + softly.assertThat(summaryDto).allMatch(dto -> !dto.isOpened()); + softly.assertThat(summaryDto).allMatch(dto -> !dto.profileUrl().isEmpty()); + softly.assertThat(summaryDto).allMatch(dto -> !dto.nickname().isEmpty()); + }); + } + + @Test + void 그룹캡슐_아이디가_아니면_그룹_캡슐의_상세_내용을_조회할_수_없다() { + //given + Long notCapsuleId = -1L; + + //when + Optional detailDto = groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId( + notCapsuleId); + + //then + assertThat(detailDto).isEmpty(); + } + + @Test + void 그룹캡슐_아이디로_그룹_캡슐의_요약_조회_하면_요약_내용을_조회할_수_있다() { + // given + //when + GroupCapsuleSummaryDto detailDto = groupCapsuleQueryRepository.findGroupCapsuleSummaryDtoByCapsuleId( + capsuleId).orElseThrow(); + CapsuleSummaryDto capsuleSummaryDto = detailDto.capsuleSummaryDto(); + + //then + SoftAssertions.assertSoftly( + softly -> { + softly.assertThat(capsuleSummaryDto).isNotNull(); + softly.assertThat(capsuleSummaryDto.title()).isEqualTo(capsule.getTitle()); + softly.assertThat(capsuleSummaryDto.point()).isEqualTo(capsule.getPoint()); + } + ); + } + + @Test + void 그룹캡슐_아이디로_그룹_캡슐의_요약_조회_하면_그룹원_정보를_조회할_수_있다() { + // given + //when + GroupCapsuleSummaryDto detailDto = groupCapsuleQueryRepository.findGroupCapsuleSummaryDtoByCapsuleId( + capsuleId).orElseThrow(); + List summaryDto = detailDto.members(); + + //then + SoftAssertions.assertSoftly( + softly -> { + softly.assertThat(summaryDto).isNotEmpty(); + softly.assertThat(summaryDto).allMatch(dto -> !dto.isOpened()); + softly.assertThat(summaryDto).allMatch(dto -> !dto.profileUrl().isEmpty()); + softly.assertThat(summaryDto).allMatch(dto -> !dto.nickname().isEmpty()); + }); + } + + @Test + void 그룹캡슐_아이디가_아니면_그룹_캡슐의_요약_내용을_조회할_수_없다() { + //given + Long notCapsuleId = -1L; + + //when + Optional detailDto = groupCapsuleQueryRepository.findGroupCapsuleSummaryDtoByCapsuleId( + notCapsuleId); + //then + assertThat(detailDto).isEmpty(); + } + + @Test + void 사용자가_사용자가_만든_그룹_캡슐을_조회하면_사용자가_만든_그룹_캡슐만_나온다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().plusDays(1); + + //when + Slice groupCapsules = groupCapsuleQueryRepository.findMyGroupCapsuleSlice( + groupLeaderId, 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/group_capsule/service/GroupCapsuleServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleServiceTest.java new file mode 100644 index 000000000..83864e475 --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/group_capsule/service/GroupCapsuleServiceTest.java @@ -0,0 +1,189 @@ +package site.timecapsulearchive.core.domain.capsule.group_capsule.service; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.time.ZonedDateTime; +import java.util.List; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; +import site.timecapsulearchive.core.common.fixture.dto.CapsuleDtoFixture; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; +import site.timecapsulearchive.core.domain.capsule.generic_capsule.repository.CapsuleRepository; +import site.timecapsulearchive.core.domain.capsule.group_capsule.data.dto.GroupCapsuleDetailDto; +import site.timecapsulearchive.core.domain.capsule.group_capsule.repository.GroupCapsuleQueryRepository; +import site.timecapsulearchive.core.domain.capsule.group_capsule.service.GroupCapsuleService; +import site.timecapsulearchive.core.domain.group.data.dto.GroupMemberSummaryDto; + +class GroupCapsuleServiceTest { + + private final Long capsuleId = 1L; + private final int groupMemberCount = 3; + + private final GroupCapsuleQueryRepository groupCapsuleQueryRepository = mock( + GroupCapsuleQueryRepository.class); + private final GroupCapsuleService groupCapsuleService; + + public GroupCapsuleServiceTest() { + CapsuleRepository capsuleRepository = mock(CapsuleRepository.class); + this.groupCapsuleService = new GroupCapsuleService(capsuleRepository, groupCapsuleQueryRepository); + } + + @Test + void 개봉된_그룹_캡슐의_상세_내용을_볼_수_있다() { + //given + given( + groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId(anyLong())).willReturn( + CapsuleDtoFixture.getGroupCapsuleDetailDto(capsuleId, true, ZonedDateTime.now(), 3) + ); + + //when + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( + capsuleId); + + + //then + SoftAssertions.assertSoftly(softly -> { + CapsuleDetailDto detailDto = response.capsuleDetailDto(); + List members = response.members(); + softly.assertThat(response).isNotNull(); + softly.assertThat(detailDto.isOpened()).isTrue(); + softly.assertThat(detailDto.title()).isNotBlank(); + softly.assertThat(detailDto.content()).isNotBlank(); + softly.assertThat(detailDto.images()).isNotBlank(); + softly.assertThat(detailDto.videos()).isNotBlank(); + softly.assertThat(members.size()).isEqualTo(groupMemberCount); + }); + } + + @Test + void 개봉일이_없고_개봉된_캡슐의_상세_내용을_볼_수_있다() { + //given + given( + groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId(anyLong())).willReturn( + CapsuleDtoFixture.getGroupCapsuleDetailDto(capsuleId, true, null, 3) + ); + + //when + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( + capsuleId); + + + //then + SoftAssertions.assertSoftly(softly -> { + CapsuleDetailDto detailDto = response.capsuleDetailDto(); + List members = response.members(); + softly.assertThat(response).isNotNull(); + softly.assertThat(detailDto.isOpened()).isTrue(); + softly.assertThat(detailDto.title()).isNotBlank(); + softly.assertThat(detailDto.content()).isNotBlank(); + softly.assertThat(detailDto.images()).isNotBlank(); + softly.assertThat(detailDto.videos()).isNotBlank(); + softly.assertThat(members.size()).isEqualTo(groupMemberCount); + }); + } + + @Test + void 개봉일이_없고_개봉되지_않은_캡슐의_상세_내용을_볼_수_있다() { + //given + given( + groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId(anyLong())).willReturn( + CapsuleDtoFixture.getGroupCapsuleDetailDto(capsuleId, false, null, 3) + ); + + //when + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( + capsuleId); + + + //then + SoftAssertions.assertSoftly(softly -> { + CapsuleDetailDto detailDto = response.capsuleDetailDto(); + List members = response.members(); + softly.assertThat(response).isNotNull(); + softly.assertThat(detailDto.isOpened()).isFalse(); + softly.assertThat(detailDto.title()).isNotBlank(); + softly.assertThat(detailDto.content()).isNotBlank(); + softly.assertThat(detailDto.images()).isNotBlank(); + softly.assertThat(detailDto.videos()).isNotBlank(); + softly.assertThat(members.size()).isEqualTo(groupMemberCount); + }); + } + + @Test + void 개봉일이_지나고_개봉되지_않은_캡슐을_조회하면_상세_내용을_볼_수_없다() { + //given + given( + groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId(anyLong())).willReturn( + CapsuleDtoFixture.getGroupCapsuleDetailDto(capsuleId, false, ZonedDateTime.now().minusDays(5), 3) + ); + + //when + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( + capsuleId); + + + //then + SoftAssertions.assertSoftly(softly -> { + CapsuleDetailDto detailDto = response.capsuleDetailDto(); + List members = response.members(); + softly.assertThat(response).isNotNull(); + softly.assertThat(detailDto.isOpened()).isFalse(); + softly.assertThat(detailDto.images()).isNullOrEmpty(); + softly.assertThat(detailDto.videos()).isNullOrEmpty(); + softly.assertThat(members.size()).isEqualTo(groupMemberCount); + }); + } + + @Test + void 개봉일이_지나지_않고_개봉되지_않은_캡슐을_조회하면_상세_내용을_볼_수_없다() { + //given + given( + groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId(anyLong())).willReturn( + CapsuleDtoFixture.getGroupCapsuleDetailDto(capsuleId, false, ZonedDateTime.now().plusDays(5), 3) + ); + + //when + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( + capsuleId); + + + //then + SoftAssertions.assertSoftly(softly -> { + CapsuleDetailDto detailDto = response.capsuleDetailDto(); + List members = response.members(); + softly.assertThat(response).isNotNull(); + softly.assertThat(detailDto.isOpened()).isFalse(); + softly.assertThat(detailDto.images()).isNullOrEmpty(); + softly.assertThat(detailDto.videos()).isNullOrEmpty(); + softly.assertThat(members.size()).isEqualTo(groupMemberCount); + }); + } + + @Test + void 개봉일이_지나지_않고_개봉된_캡슐을_조회하면_상세_내용을_볼_수_없다() { + //given + given( + groupCapsuleQueryRepository.findGroupCapsuleDetailDtoByCapsuleId(anyLong())).willReturn( + CapsuleDtoFixture.getGroupCapsuleDetailDto(capsuleId, true, ZonedDateTime.now().plusDays(5), 3) + ); + + //when + GroupCapsuleDetailDto response = groupCapsuleService.findGroupCapsuleDetailByGroupIdAndCapsuleId( + capsuleId); + + + //then + SoftAssertions.assertSoftly(softly -> { + CapsuleDetailDto detailDto = response.capsuleDetailDto(); + List members = response.members(); + softly.assertThat(response).isNotNull(); + softly.assertThat(detailDto.isOpened()).isTrue(); + softly.assertThat(detailDto.images()).isNullOrEmpty(); + softly.assertThat(detailDto.videos()).isNullOrEmpty(); + softly.assertThat(members.size()).isEqualTo(groupMemberCount); + }); + } + +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/media/repository/MediaQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/media/repository/MediaQueryRepositoryTest.java index 9d865cb3e..881a3465f 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/media/repository/MediaQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/media/repository/MediaQueryRepositoryTest.java @@ -14,11 +14,11 @@ 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.CapsuleFixture; -import site.timecapsulearchive.core.common.fixture.CapsuleSkinFixture; -import site.timecapsulearchive.core.common.fixture.ImageFixture; -import site.timecapsulearchive.core.common.fixture.MemberFixture; -import site.timecapsulearchive.core.common.fixture.VideoFixture; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleFixture; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleSkinFixture; +import site.timecapsulearchive.core.common.fixture.domain.ImageFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.VideoFixture; import site.timecapsulearchive.core.domain.capsule.entity.Capsule; import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; import site.timecapsulearchive.core.domain.capsule.entity.Image; 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/public_capsule/repository/PublicCapsuleQueryRepositoryTest.java similarity index 58% rename from backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/PublicCapsuleQueryRepositoryTest.java rename to backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/public_capsule/repository/PublicCapsuleQueryRepositoryTest.java index 8b0a1cb06..f31b91a8a 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/public_capsule/repository/PublicCapsuleQueryRepositoryTest.java @@ -1,4 +1,4 @@ -package site.timecapsulearchive.core.domain.capsule.repository; +package site.timecapsulearchive.core.domain.capsule.public_capsule.repository; import static org.assertj.core.api.Assertions.assertThat; @@ -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; @@ -14,14 +15,15 @@ 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.CapsuleFixture; -import site.timecapsulearchive.core.common.fixture.CapsuleSkinFixture; -import site.timecapsulearchive.core.common.fixture.MemberFixture; -import site.timecapsulearchive.core.common.fixture.MemberFriendFixture; +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.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/PublicCapsuleServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/public_capsule/service/PublicCapsuleServiceTest.java similarity index 78% rename from backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/service/PublicCapsuleServiceTest.java rename to backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/public_capsule/service/PublicCapsuleServiceTest.java index 8fcfb780f..009771110 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/service/PublicCapsuleServiceTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/public_capsule/service/PublicCapsuleServiceTest.java @@ -1,15 +1,14 @@ -package site.timecapsulearchive.core.domain.capsule.service; +package site.timecapsulearchive.core.domain.capsule.public_capsule.service; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import java.time.ZonedDateTime; -import java.util.Optional; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; -import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.common.fixture.dto.CapsuleDtoFixture; import site.timecapsulearchive.core.domain.capsule.generic_capsule.data.dto.CapsuleDetailDto; import site.timecapsulearchive.core.domain.capsule.public_capsule.repository.PublicCapsuleQueryRepository; import site.timecapsulearchive.core.domain.capsule.public_capsule.service.PublicCapsuleService; @@ -19,6 +18,9 @@ @Import({S3Config.class, GeoTransformConfig.class}) class PublicCapsuleServiceTest { + private final Long memberId = 1L; + private final Long capsuleId = 1L; + private final PublicCapsuleQueryRepository publicCapsuleQueryRepository = mock( PublicCapsuleQueryRepository.class); private final PublicCapsuleService publicCapsuleService; @@ -30,11 +32,10 @@ class PublicCapsuleServiceTest { @Test void 개봉된_캡슐을_조회하면_모든_내용을_볼_수_있다() { //given - Long memberId = 1L; - Long capsuleId = 1L; given(publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( anyLong(), anyLong())) - .willReturn(getCapsuleDetailDto(capsuleId, true, ZonedDateTime.now())); + .willReturn( + CapsuleDtoFixture.getCapsuleDetailDto(capsuleId, true, ZonedDateTime.now())); //when CapsuleDetailDto response = publicCapsuleService.findPublicCapsuleDetailByMemberIdAndCapsuleId( @@ -43,6 +44,7 @@ class PublicCapsuleServiceTest { //then SoftAssertions.assertSoftly(softly -> { softly.assertThat(response).isNotNull(); + softly.assertThat(response.isOpened()).isTrue(); softly.assertThat(response.title()).isNotBlank(); softly.assertThat(response.content()).isNotBlank(); softly.assertThat(response.images()).isNotBlank(); @@ -50,24 +52,12 @@ class PublicCapsuleServiceTest { }); } - private Optional getCapsuleDetailDto(Long capsuleId, Boolean isOpened, - ZonedDateTime dueDate) { - ZonedDateTime now = ZonedDateTime.now(); - - return Optional.of( - new CapsuleDetailDto(capsuleId, "test", dueDate, "test", "test", now, "address", - "roadName", "title", "content", "images", "videos", isOpened, CapsuleType.PUBLIC) - ); - } - @Test void 개봉일이_없고_개봉된_캡슐을_조회하면_모든_내용을_볼_수_있다() { //given - Long memberId = 1L; - Long capsuleId = 1L; given(publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( anyLong(), anyLong())) - .willReturn(getCapsuleDetailDto(capsuleId, true, null)); + .willReturn(CapsuleDtoFixture.getCapsuleDetailDto(capsuleId, true, null)); //when CapsuleDetailDto response = publicCapsuleService.findPublicCapsuleDetailByMemberIdAndCapsuleId( @@ -76,6 +66,7 @@ private Optional getCapsuleDetailDto(Long capsuleId, Boolean i //then SoftAssertions.assertSoftly(softly -> { softly.assertThat(response).isNotNull(); + softly.assertThat(response.isOpened()).isTrue(); softly.assertThat(response.title()).isNotBlank(); softly.assertThat(response.content()).isNotBlank(); softly.assertThat(response.images()).isNotBlank(); @@ -86,11 +77,9 @@ private Optional getCapsuleDetailDto(Long capsuleId, Boolean i @Test void 개봉일이_없고_개봉되지_않은_캡슐을_조회하면_모든_내용을_볼_수_있다() { //given - Long memberId = 1L; - Long capsuleId = 1L; given(publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( anyLong(), anyLong())) - .willReturn(getCapsuleDetailDto(capsuleId, false, null)); + .willReturn(CapsuleDtoFixture.getCapsuleDetailDto(capsuleId, false, null)); //when CapsuleDetailDto response = publicCapsuleService.findPublicCapsuleDetailByMemberIdAndCapsuleId( @@ -99,6 +88,7 @@ private Optional getCapsuleDetailDto(Long capsuleId, Boolean i //then SoftAssertions.assertSoftly(softly -> { softly.assertThat(response).isNotNull(); + softly.assertThat(response.isOpened()).isFalse(); softly.assertThat(response.title()).isNotBlank(); softly.assertThat(response.content()).isNotBlank(); softly.assertThat(response.images()).isNotBlank(); @@ -107,13 +97,12 @@ private Optional getCapsuleDetailDto(Long capsuleId, Boolean i } @Test - void 개봉일이_지난_개봉되지_않은_캡슐을_조회하면_모든_내용을_볼_수_없다() { + void 개봉일이_지나고_개봉되지_않은_캡슐을_조회하면_모든_내용을_볼_수_없다() { //given - Long memberId = 1L; - Long capsuleId = 1L; given(publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( anyLong(), anyLong())) - .willReturn(getCapsuleDetailDto(capsuleId, false, ZonedDateTime.now().minusDays(5))); + .willReturn(CapsuleDtoFixture.getCapsuleDetailDto(capsuleId, false, + ZonedDateTime.now().minusDays(5))); //when CapsuleDetailDto response = publicCapsuleService.findPublicCapsuleDetailByMemberIdAndCapsuleId( @@ -131,11 +120,10 @@ private Optional getCapsuleDetailDto(Long capsuleId, Boolean i @Test void 개봉일이_지나지_않고_개봉되지_않은_캡슐을_조회하면_모든_내용을_볼_수_없다() { //given - Long memberId = 1L; - Long capsuleId = 1L; given(publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( anyLong(), anyLong())) - .willReturn(getCapsuleDetailDto(capsuleId, false, ZonedDateTime.now().plusDays(5))); + .willReturn(CapsuleDtoFixture.getCapsuleDetailDto(capsuleId, false, + ZonedDateTime.now().plusDays(5))); //when CapsuleDetailDto response = publicCapsuleService.findPublicCapsuleDetailByMemberIdAndCapsuleId( @@ -153,11 +141,10 @@ private Optional getCapsuleDetailDto(Long capsuleId, Boolean i @Test void 개봉일이_지나지_않고_개봉된_캡슐을_조회하면_모든_내용을_볼_수_없다() { //given - Long memberId = 1L; - Long capsuleId = 1L; given(publicCapsuleQueryRepository.findPublicCapsuleDetailDtosByMemberIdAndCapsuleId( anyLong(), anyLong())) - .willReturn(getCapsuleDetailDto(capsuleId, true, ZonedDateTime.now().plusDays(5))); + .willReturn(CapsuleDtoFixture.getCapsuleDetailDto(capsuleId, true, + ZonedDateTime.now().plusDays(5))); //when CapsuleDetailDto response = publicCapsuleService.findPublicCapsuleDetailByMemberIdAndCapsuleId( diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java new file mode 100644 index 000000000..ccb33429c --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/capsule/repository/GroupCapsuleOpenQueryRepositoryTest.java @@ -0,0 +1,86 @@ +package site.timecapsulearchive.core.domain.capsule.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.persistence.EntityManager; +import java.util.List; +import javax.sql.DataSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.TestConstructor; +import org.springframework.test.context.TestConstructor.AutowireMode; +import org.springframework.transaction.annotation.Transactional; +import site.timecapsulearchive.core.common.RepositoryTest; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleFixture; +import site.timecapsulearchive.core.common.fixture.domain.CapsuleSkinFixture; +import site.timecapsulearchive.core.common.fixture.domain.GroupCapsuleOpenFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.domain.capsule.entity.Capsule; +import site.timecapsulearchive.core.domain.capsule.entity.CapsuleType; +import site.timecapsulearchive.core.domain.capsule.entity.GroupCapsuleOpen; +import site.timecapsulearchive.core.domain.capsule.group_capsule.repository.GroupCapsuleOpenQueryRepository; +import site.timecapsulearchive.core.domain.capsuleskin.entity.CapsuleSkin; +import site.timecapsulearchive.core.domain.member.entity.Member; + +@TestConstructor(autowireMode = AutowireMode.ALL) +class GroupCapsuleOpenQueryRepositoryTest extends RepositoryTest { + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final GroupCapsuleOpenQueryRepository groupCapsuleOpenRepository; + private Capsule capsule; + private List groupMembers; + private List groupCapsuleOpens; + + public GroupCapsuleOpenQueryRepositoryTest( + JdbcTemplate jdbcTemplate, + DataSource dataSource + ) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + this.groupCapsuleOpenRepository = new GroupCapsuleOpenQueryRepository(jdbcTemplate); + } + + @Transactional + @BeforeEach + void setUp(@Autowired EntityManager entityManager) { + groupMembers = MemberFixture.members(1, 5); + groupMembers.forEach(entityManager::persist); + + Member groupLeader = groupMembers.get(0); + + CapsuleSkin capsuleSkin = CapsuleSkinFixture.capsuleSkin(groupLeader); + entityManager.persist(capsuleSkin); + + capsule = CapsuleFixture.capsule(groupLeader, capsuleSkin, CapsuleType.GROUP); + entityManager.persist(capsule); + + groupCapsuleOpens = GroupCapsuleOpenFixture.groupCapsuleOpens(false, capsule, groupMembers); + } + + @Test + void 그룹캡슐이_생성되면_그룹원들은_그룹_캡슐_오픈_여부를_저장할_수_있다() { + // given + List groupMemberIds = groupMembers.stream().map(Member::getId).toList(); + Long capsuleId = capsule.getId(); + + // when + groupCapsuleOpenRepository.bulkSave(groupMemberIds, capsule); + + //then + String sql = "SELECT count(*) from group_capsule_open WHERE capsule_id = (:capsuleId) and member_id in (:groupMemberIds)"; + MapSqlParameterSource parameters = new MapSqlParameterSource("groupMemberIds", + groupMemberIds); + parameters.addValue("capsuleId", capsuleId); + + Integer actualGroupMemberCount = namedParameterJdbcTemplate.queryForObject( + sql, + parameters, + Integer.class + ); + + assertThat(actualGroupMemberCount).isEqualTo(groupCapsuleOpens.size()); + } +} diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepositoryTest.java index a0b786700..3c86b59b8 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/FriendInviteQueryRepositoryTest.java @@ -12,7 +12,7 @@ import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestConstructor.AutowireMode; import site.timecapsulearchive.core.common.RepositoryTest; -import site.timecapsulearchive.core.common.fixture.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -35,11 +35,8 @@ void setup() { owner = MemberFixture.member(0); entityManager.persist(owner); - for (int i = 1; i < 21; i++) { - Member friend = MemberFixture.member(i); - friends.add(friend); - entityManager.persist(friend); - } + friends.addAll(MemberFixture.members(1, 11)); + friends.forEach(entityManager::persist); } @Test diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepositoryTest.java index 090ca458c..c82512797 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendQueryRepositoryTest.java @@ -21,12 +21,12 @@ 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.FriendInviteFixture; -import site.timecapsulearchive.core.common.fixture.MemberFixture; -import site.timecapsulearchive.core.common.fixture.MemberFriendFixture; +import site.timecapsulearchive.core.common.fixture.domain.FriendInviteFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFriendFixture; import site.timecapsulearchive.core.domain.friend.data.dto.FriendSummaryDto; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; -import site.timecapsulearchive.core.domain.friend.data.dto.SearchTagFriendSummaryDto; +import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; import site.timecapsulearchive.core.domain.friend.entity.FriendInvite; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; import site.timecapsulearchive.core.domain.member.entity.Member; @@ -34,17 +34,18 @@ @TestConstructor(autowireMode = AutowireMode.ALL) class MemberFriendQueryRepositoryTest extends RepositoryTest { - private static final int MAX_FRIEND_ID = 21; - private static final int MAX_MEMBER_ID = 31; + private static final int MAX_COUNT = 10; + private static final Long FRIEND_START_ID = 1L; + private static final Long FRIEND_ID_TO_INVITE_OWNER = 11L; + private static final Long NOT_FRIEND_MEMBER_START_ID = 12L; private final List hashedNotMemberPhones = new ArrayList<>(); private final List hashedFriendPhones = new ArrayList<>(); private final List hashedNotFriendPhones = new ArrayList<>(); - private final MemberFriendQueryRepository memberFriendQueryRepository; - private Long ownerId; private Long friendId; + private final MemberFriendQueryRepository memberFriendQueryRepository; MemberFriendQueryRepositoryTest(EntityManager entityManager) { this.memberFriendQueryRepository = new MemberFriendQueryRepository( @@ -58,8 +59,8 @@ void setup(@Autowired EntityManager entityManager) { entityManager.persist(owner); ownerId = owner.getId(); - List friends = MemberFixture.members(1, MAX_FRIEND_ID - 1); - //owner와 친구, 친구 초대 데이터 + // owner와 친구 관계를 맺는 데이터 + List friends = MemberFixture.members(FRIEND_START_ID.intValue(), MAX_COUNT); for (Member friend : friends) { entityManager.persist(friend); hashedFriendPhones.add(friend.getPhone_hash()); @@ -69,29 +70,31 @@ void setup(@Autowired EntityManager entityManager) { MemberFriend friendMember = MemberFriendFixture.memberFriend(friend, owner); entityManager.persist(friendMember); - - //friend -> owner 친구 요청 - FriendInvite friendInvite = FriendInviteFixture.friendInvite(friend, owner); - entityManager.persist(friendInvite); } + friendId = friends.get(0).getId(); - //friend id - friendId = friends.get(friends.size() - 1).getId(); + // owner에게 요청만 보낸 데이터 + Member inviteFriendToOwner = MemberFixture.member(FRIEND_ID_TO_INVITE_OWNER.intValue()); + entityManager.persist(inviteFriendToOwner); + + FriendInvite friendInvite = FriendInviteFixture.friendInvite(inviteFriendToOwner, owner); + entityManager.persist(friendInvite); //owner와 친구가 아닌 멤버 데이터 - List members = MemberFixture.members(MAX_FRIEND_ID, 10); - for (Member notFriend : members) { + List notFriendMembers = MemberFixture.members(NOT_FRIEND_MEMBER_START_ID.intValue(), + MAX_COUNT); + for (Member notFriend : notFriendMembers) { entityManager.persist(notFriend); hashedNotFriendPhones.add(notFriend.getPhone_hash()); } //회원이 아닌 휴대전화번호 데이터 - hashedNotMemberPhones.addAll(MemberFixture.getPhoneBytesList(MAX_MEMBER_ID + 20, 10)); + hashedNotMemberPhones.addAll(MemberFixture.getPhoneBytesList(23, MAX_COUNT)); } @ParameterizedTest - @ValueSource(ints = {20, 15, 10, 5}) - void 특정_사용자로_친구_목록_조회하면_친구_목록_리스트가_나온다(int size) { + @ValueSource(ints = {2, 7, 10, 5}) + void 사용자가_친구_목록_조회하면_친구_관계를_맺은_사용자_리스트가_나온다(int size) { //given ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(5); @@ -114,10 +117,10 @@ void setup(@Autowired EntityManager entityManager) { } @Test - void 친구_요청만_보낸_사용자의_친구_목록_조회하면_빈_리스트가_나온다() { + void 친구가_친구_목록_조회하면_친구_관계를_맺은_사용자_리스트가_나온다() { //given - int size = 20; - ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(5); + int size = 10; + ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(5); //when Slice slice = memberFriendQueryRepository.findFriendsSlice( @@ -126,29 +129,28 @@ void setup(@Autowired EntityManager entityManager) { now ); - //then - assertThat(slice.getContent().size()).isEqualTo(0); + assertThat(slice.getContent().size()).isEqualTo(1); } @Test - void 유효하지_않은_시간으로_사용자의_친구_목록_조회하면_빈_리스트가_나온다() { + void 친구_요청만_보낸_사용자가_친구_목록_조회하면_빈_리스트가_나온다() { //given int size = 20; - ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(5); + ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(5); //when Slice slice = memberFriendQueryRepository.findFriendsSlice( - ownerId, + FRIEND_ID_TO_INVITE_OWNER, size, now ); //then - assertThat(slice.getContent().size()).isEqualTo(0); + assertThat(slice).isEmpty(); } @Test - void 친구가_없는_사용자로_친구_목록_조회하면_빈_리스트가_나온다() { + void 사용자가_유효하지_않은_시간으로_사용자의_친구_목록_조회하면_빈_리스트가_나온다() { //given int size = 20; ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(5); @@ -161,35 +163,28 @@ void setup(@Autowired EntityManager entityManager) { ); //then - assertThat(slice.getContent().size()).isEqualTo(0); + assertThat(slice).isEmpty(); } - @ParameterizedTest - @ValueSource(ints = {20, 15, 10, 5}) - void 특정_사용자로_친구_요청_목록_조회하면_친구_요청_목록_리스트가_나온다(int size) { + @Test + void 친구가_없는_사용자가_친구_목록_조회하면_빈_리스트가_나온다() { //given + int size = 20; ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(5); //when - Slice slice = memberFriendQueryRepository.findFriendRequestsSlice( - ownerId, + Slice slice = memberFriendQueryRepository.findFriendsSlice( + NOT_FRIEND_MEMBER_START_ID, size, now ); //then - assertSoftly(softly -> { - softly.assertThat(slice.getContent().size()).isEqualTo(size); - softly.assertThat(slice.getContent()).allMatch(dto -> dto.createdAt().isBefore(now)); - softly.assertThat(slice.getContent()).allMatch(dto -> Objects.nonNull(dto.id())); - softly.assertThat(slice.getContent()).allMatch(dto -> !dto.profileUrl().isBlank()); - softly.assertThat(slice.getContent()).allMatch(dto -> !dto.nickname().isBlank()); - } - ); + assertThat(slice).isEmpty(); } @Test - void 유효하지_않은_시간으로_사용자의_친구_요청_목록_조회하면_빈_리스트가_나온다() { + void 사용자가_유효하지_않은_시간으로_사용자의_친구_요청_목록_조회하면_빈_리스트가_나온다() { //given int size = 20; ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(5); @@ -202,28 +197,28 @@ void setup(@Autowired EntityManager entityManager) { ); //then - assertThat(slice.getContent().size()).isEqualTo(0); + assertThat(slice).isEmpty(); } @Test - void 친구가_없는_사용자로_친구_요청_목록_조회하면_빈_리스트가_나온다() { + void 친구가_없는_사용자가_친구_요청_목록_조회하면_빈_리스트가_나온다() { //given int size = 20; - ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(5); + ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(5); //when Slice slice = memberFriendQueryRepository.findFriendRequestsSlice( - ownerId, + NOT_FRIEND_MEMBER_START_ID, size, now ); //then - assertThat(slice.getContent().size()).isEqualTo(0); + assertThat(slice).isEmpty(); } @Test - void 앱_사용자의_전화번호가_아닌_경우_주소록_기반_사용자_리스트_조회하면_빈_리스트가_나온다() { + void 사용자가_앱_사용자의_전화번호가_아닌_경우_주소록_기반_사용자_리스트_조회하면_빈_리스트가_나온다() { //given //when List friends = memberFriendQueryRepository.findFriendsByPhone( @@ -231,11 +226,11 @@ void setup(@Autowired EntityManager entityManager) { hashedNotMemberPhones); //then - assertThat(friends.size()).isSameAs(0); + assertThat(friends).isEmpty(); } @Test - void 친구인_사용자로_주소록_기반_사용자_리스트_조회하면_친구인_앱_사용자_리스트가_나온다() { + void 사용자가_친구인_사용자로_주소록_기반_사용자_리스트_조회하면_친구인_앱_사용자_리스트가_나온다() { //given //when List friends = memberFriendQueryRepository.findFriendsByPhone( @@ -244,13 +239,13 @@ void setup(@Autowired EntityManager entityManager) { //then assertSoftly(softly -> { - softly.assertThat(friends.size()).isSameAs(MAX_FRIEND_ID - 1); + softly.assertThat(friends.size()).isSameAs(MAX_COUNT); softly.assertThat(friends).allMatch(friend -> friend.isFriend() == Boolean.TRUE); }); } @Test - void 친구가_아닌_사용자로_주소록_기반_사용자_리스트_조회하면_앱_사용자_리스트가_나온다() { + void 사용자가_친구가_아닌_사용자로_주소록_기반_사용자_리스트_조회하면_앱_사용자_리스트가_나온다() { //given //when List friends = memberFriendQueryRepository.findFriendsByPhone( @@ -259,13 +254,13 @@ void setup(@Autowired EntityManager entityManager) { //then assertSoftly(softly -> { - assertThat(friends.size()).isSameAs(MAX_MEMBER_ID - MAX_FRIEND_ID); + assertThat(friends.size()).isSameAs(MAX_COUNT); softly.assertThat(friends).allMatch(friend -> friend.isFriend() == Boolean.FALSE); }); } @Test - void 빈_전화번호_목록으로_주소록_기반_사용자_리스트_조회하면_빈_리스트가_나온다() { + void 사용자가_빈_전화번호_목록으로_주소록_기반_사용자_리스트_조회하면_빈_리스트가_나온다() { //given List phoneHashes = Collections.emptyList(); @@ -275,14 +270,14 @@ void setup(@Autowired EntityManager entityManager) { phoneHashes); //then - assertThat(friends.size()).isSameAs(0); + assertThat(friends).isEmpty(); } @ParameterizedTest - @ValueSource(ints = {20, 15, 10, 5}) - void 사용자_아이디와_친구_태그로_친구관계를_조회하면_친구인_경우_True를_반환한다(int friendId) { + @ValueSource(ints = {3, 6, 10, 5}) + void 사용자가_친구_태그로_친구관계를_조회하면_친구인_경우_True를_반환한다(int friendId) { //given - SearchTagFriendSummaryDto dto = memberFriendQueryRepository.findFriendsByTag( + SearchFriendSummaryDtoByTag dto = memberFriendQueryRepository.findFriendsByTag( ownerId, friendId + "testTag").orElseThrow(); //when @@ -293,10 +288,10 @@ void setup(@Autowired EntityManager entityManager) { } @ParameterizedTest - @ValueSource(ints = {30, 28, 25, 21}) - void 사용자_아이디와_친구_태그로_친구관계를_조회하면_친구가_아닌_경우_False를_반환한다(int friendId) { + @ValueSource(ints = {12, 15, 19, 21}) + void 사용자가_친구_태그로_친구관계를_조회하면_친구가_아닌_경우_False를_반환한다(int friendId) { //given - SearchTagFriendSummaryDto dto = memberFriendQueryRepository.findFriendsByTag( + SearchFriendSummaryDtoByTag dto = memberFriendQueryRepository.findFriendsByTag( ownerId, friendId + "testTag").orElseThrow(); //when diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendRepositoryTest.java index e117e0f59..7b4b9bf6d 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/repository/MemberFriendRepositoryTest.java @@ -12,8 +12,8 @@ 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.MemberFixture; -import site.timecapsulearchive.core.common.fixture.MemberFriendFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFriendFixture; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; import site.timecapsulearchive.core.domain.member.entity.Member; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/FriendServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/FriendServiceTest.java index e8cebbfb5..4a20b1fcb 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/FriendServiceTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/friend/service/FriendServiceTest.java @@ -1,18 +1,26 @@ package site.timecapsulearchive.core.domain.friend.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; import org.springframework.transaction.support.TransactionTemplate; -import site.timecapsulearchive.core.common.fixture.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.dto.FriendDtoFixture; import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDto; +import site.timecapsulearchive.core.domain.friend.data.dto.SearchFriendSummaryDtoByTag; +import site.timecapsulearchive.core.domain.friend.data.response.SearchTagFriendSummaryResponse; +import site.timecapsulearchive.core.domain.friend.exception.FriendNotFoundException; import site.timecapsulearchive.core.domain.friend.repository.FriendInviteQueryRepository; import site.timecapsulearchive.core.domain.friend.repository.FriendInviteRepository; import site.timecapsulearchive.core.domain.friend.repository.MemberFriendQueryRepository; @@ -47,12 +55,13 @@ class FriendServiceTest { ); @Test - void 앱_사용자_핸드폰_번호로_주소록_기반_사용자_리스트_조회_테스트() { + void 사용자는_주소록_기반_핸드폰_번호로_Ahchive_사용자_리스트를_조회_할_수_있다() { //given Long memberId = 1L; - List phones = MemberFixture.getPhones(); + List phones = MemberFixture.getPhones(5); + given(memberFriendQueryRepository.findFriendsByPhone(anyLong(), anyList())) - .willReturn(getFriendSummaryDtos()); + .willReturn(FriendDtoFixture.getFriendSummaryDtos(5)); //when List dtos = friendService.findFriendsByPhone(memberId, phones); @@ -61,19 +70,9 @@ class FriendServiceTest { assertThat(dtos.size()).isEqualTo(phones.size()); } - private List getFriendSummaryDtos() { - List result = new ArrayList<>(); - for (long i = 0; i < 8; i++) { - result.add(new SearchFriendSummaryDto(i, i + "testProfile.com", i + "testNickname", - new ByteArrayWrapper(MemberFixture.getPhoneBytes((int) i)), - Boolean.TRUE, Boolean.FALSE)); - } - - return result; - } @Test - void 번호_없이_주소록_기반_사용자_리스트_조회_테스트() { + void 사용자는_주소록_기반_번호_없이_Ahchive_사용자_리스트_조회하면_빈_리스트를_반환한다() { //given Long memberId = 1L; List phones = Collections.emptyList(); @@ -85,6 +84,44 @@ private List getFriendSummaryDtos() { phones); //then - assertThat(dtos.size()).isEqualTo(0); + assertTrue(dtos.isEmpty()); + } + + @Test + void 사용자는_태그로_Ahchive_사용자를_검색할_수_있다() { + //given + Long memberId = 1L; + String tag = "testTag"; + Optional summaryDtoByTag = FriendDtoFixture.getFriendSummaryDtoByTag(); + SearchFriendSummaryDtoByTag expectDto = summaryDtoByTag.get(); + + given(memberFriendQueryRepository.findFriendsByTag(anyLong(), anyString())) + .willReturn(summaryDtoByTag); + + //when + SearchTagFriendSummaryResponse actualResponse = friendService.searchFriend( + memberId, tag); + + //then + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(expectDto.id()).isEqualTo(actualResponse.id()); + softly.assertThat(expectDto.profileUrl()).isEqualTo(actualResponse.profileUrl()); + softly.assertThat(expectDto.nickname()).isEqualTo(actualResponse.nickname()); + softly.assertThat(expectDto.isFriend()).isEqualTo(actualResponse.isFriend()); + }); + } + + @Test + void 사용자는_존재하지_않는_태그로_Ahchive_사용자를_검색하면_예외가_발생한다() { + //given + Long memberId = 1L; + String tag = "testTag"; + + given(memberFriendQueryRepository.findFriendsByTag(anyLong(), anyString())) + .willReturn(Optional.empty()); + + //when + assertThatThrownBy(() -> friendService.searchFriend(memberId, tag)) + .isInstanceOf(FriendNotFoundException.class); } } \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java new file mode 100644 index 000000000..bb5f408de --- /dev/null +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/group/repository/MemberGroupQueryRepositoryTest.java @@ -0,0 +1,210 @@ +package site.timecapsulearchive.core.domain.group.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Slice; +import org.springframework.test.context.TestConstructor; +import org.springframework.test.context.TestConstructor.AutowireMode; +import org.springframework.transaction.annotation.Transactional; +import site.timecapsulearchive.core.common.RepositoryTest; +import site.timecapsulearchive.core.common.fixture.domain.GroupFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberGroupFixture; +import site.timecapsulearchive.core.domain.group.data.dto.GroupDetailDto; +import site.timecapsulearchive.core.domain.group.data.dto.GroupSummaryDto; +import site.timecapsulearchive.core.domain.group.entity.Group; +import site.timecapsulearchive.core.domain.group.entity.MemberGroup; +import site.timecapsulearchive.core.domain.member.entity.Member; + +@TestConstructor(autowireMode = AutowireMode.ALL) +class MemberGroupQueryRepositoryTest extends RepositoryTest { + + private final static int GROUP_COUNT = 20; + + private final GroupQueryRepository groupQueryRepository; + + private Long memberId; + private Long memberIdWithNoGroup; + private Long ownerGroupId; + + MemberGroupQueryRepositoryTest(JPAQueryFactory jpaQueryFactory) { + this.groupQueryRepository = new GroupQueryRepository(jpaQueryFactory); + } + + @Transactional + @BeforeEach + void setup(@Autowired EntityManager entityManager) { + //사용자 + Member member = MemberFixture.member(0); + entityManager.persist(member); + memberId = member.getId(); + + //그룹이 없는 사용자 + Member memberNotInGroup = MemberFixture.member(1); + entityManager.persist(memberNotInGroup); + memberIdWithNoGroup = memberNotInGroup.getId(); + + //그룹 + List groups = new ArrayList<>(); + for (int count = 0; count < GROUP_COUNT; count++) { + Group group = GroupFixture.group(); + entityManager.persist(group); + groups.add(group); + } + //사용자가 그룹장인 그룹 + ownerGroupId = groups.get(0).getId(); + + //그룹원들 + List members = MemberFixture.members(4, 2); + members.forEach(entityManager::persist); + + //그룹에 사용자를 그룹장으로 설정 + for (int count = 0; count < GROUP_COUNT; count++) { + MemberGroup memberGroup = MemberGroupFixture.memberGroup(member, groups.get(count), + Boolean.TRUE); + entityManager.persist(memberGroup); + } + + //그룹원들 설정 + List memberGroups = MemberGroupFixture.memberGroups(members, groups.get(0)); + memberGroups.forEach(entityManager::persist); + } + + @ParameterizedTest + @ValueSource(ints = {1, 5, 15, 20}) + void 사용자와_개수_마지막_데이터_생성_시간으로_그룹_목록을_조회하면_개수만큼_그룹이_반환된다(int size) { + //given + ZonedDateTime now = ZonedDateTime.now().plusDays(3); + + //when + Slice groupsSlice = groupQueryRepository.findGroupsSlice(memberId, + size, now); + + //then + assertThat(groupsSlice.getNumberOfElements()).isEqualTo(size); + } + + @Test + void 사용자와_개수_마지막_데이터_생성_시간으로_그룹_목록을_조회하면_개수만큼_그룹의_내용들이_반환된다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().plusDays(3); + + //when + List groupsSlice = groupQueryRepository.findGroupsSlice(memberId, + size, now).getContent(); + + //then + assertSoftly(softly -> { + softly.assertThat(groupsSlice).allMatch(dto -> dto.id() != null); + softly.assertThat(groupsSlice).allMatch(dto -> !dto.groupName().isBlank()); + softly.assertThat(groupsSlice).allMatch(dto -> !dto.groupDescription().isBlank()); + softly.assertThat(groupsSlice).allMatch(dto -> !dto.groupProfileUrl().isBlank()); + softly.assertThat(groupsSlice).allMatch(dto -> dto.isOwner() != null); + }); + } + + @Test + void 그룹이_없는_사용자로_그룹_목록을_조회하면_빈_그룹_목록이_반환된다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().plusDays(3); + + //when + List groupsSlice = groupQueryRepository.findGroupsSlice( + memberIdWithNoGroup, + size, + now) + .getContent(); + + //then + assertThat(groupsSlice.isEmpty()).isTrue(); + } + + @Test + void 사용자와_범위에_없는_마지막_데이터_생성_시간으로_그룹_목록을_조회하면_빈_그룹_목록이_반환된다() { + //given + int size = 20; + ZonedDateTime now = ZonedDateTime.now().minusDays(5); + + //when + List groupsSlice = groupQueryRepository.findGroupsSlice(memberId, + size, now).getContent(); + + //then + assertThat(groupsSlice.isEmpty()).isTrue(); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_그룹_상세가_반환된다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + assertThat(groupDetail).isNotNull(); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_그룹_정보를_볼_수_있다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(groupDetail.groupName()).isNotBlank(); + assertThat(groupDetail.groupDescription()).isNotBlank(); + assertThat(groupDetail.groupProfileUrl()).isNotBlank(); + assertThat(groupDetail.createdAt()).isNotNull(); + }); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_그룹원들의_정보를_볼_수_있다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(groupDetail.members()).isNotEmpty(); + assertThat(groupDetail.members()).allSatisfy(m -> assertThat(m.memberId()).isNotNull()); + assertThat(groupDetail.members()).allSatisfy(m -> assertThat(m.tag()).isNotBlank()); + assertThat(groupDetail.members()).allSatisfy( + m -> assertThat(m.nickname()).isNotBlank()); + assertThat(groupDetail.members()).allSatisfy( + m -> assertThat(m.profileUrl()).isNotBlank()); + assertThat(groupDetail.members()).allSatisfy(m -> assertThat(m.isOwner()).isNotNull()); + }); + } + + @Test + void 그룹_아이디로_그룹을_조회하면_한_명의_그룹장만_존재한다() { + //given + //when + GroupDetailDto groupDetail = groupQueryRepository.findGroupDetailByGroupId( + ownerGroupId).orElseThrow(); + + //then + SoftAssertions.assertSoftly(softly -> { + assertThat(groupDetail.members()).satisfiesOnlyOnce( + m -> assertThat(m.isOwner()).isTrue()); + }); + } +} \ No newline at end of file diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/repository/MemberQueryRepositoryTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/repository/MemberQueryRepositoryTest.java index b74d99983..8686e9114 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/repository/MemberQueryRepositoryTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/repository/MemberQueryRepositoryTest.java @@ -17,10 +17,10 @@ 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.MemberFixture; -import site.timecapsulearchive.core.common.fixture.MemberFriendFixture; -import site.timecapsulearchive.core.common.fixture.NotificationCategoryFixture; -import site.timecapsulearchive.core.common.fixture.NotificationFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFixture; +import site.timecapsulearchive.core.common.fixture.domain.MemberFriendFixture; +import site.timecapsulearchive.core.common.fixture.domain.NotificationCategoryFixture; +import site.timecapsulearchive.core.common.fixture.domain.NotificationFixture; import site.timecapsulearchive.core.domain.friend.entity.MemberFriend; import site.timecapsulearchive.core.domain.member.data.dto.MemberDetailDto; import site.timecapsulearchive.core.domain.member.data.dto.MemberNotificationDto; diff --git a/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/service/MemberServiceTest.java b/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/service/MemberServiceTest.java index 04dea089b..34d20f782 100644 --- a/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/service/MemberServiceTest.java +++ b/backend/core/src/test/java/site/timecapsulearchive/core/domain/member/service/MemberServiceTest.java @@ -17,16 +17,19 @@ import site.timecapsulearchive.core.domain.member.exception.NotVerifiedMemberException; import site.timecapsulearchive.core.domain.member.repository.MemberQueryRepository; import site.timecapsulearchive.core.domain.member.repository.MemberRepository; +import site.timecapsulearchive.core.domain.member.repository.MemberTemporaryRepository; class MemberServiceTest { private final MemberRepository memberRepository = mock(MemberRepository.class); + private final MemberTemporaryRepository memberTemporaryRepository = mock(MemberTemporaryRepository.class); private final MemberQueryRepository memberQueryRepository = mock(MemberQueryRepository.class); private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); private final MemberMapper memberMapper = mock(MemberMapper.class); private final MemberService memberService = new MemberService( memberRepository, + memberTemporaryRepository, memberQueryRepository, passwordEncoder, memberMapper