From 10608c0d0816723e0eed27941f182a17b94688f2 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Sat, 2 Mar 2024 00:43:38 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20#120=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EA=B8=B0=EC=A1=B4=20=EC=84=A4=EC=A0=95=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 1 + .../fitapet/api/apis/profile/controller/AccountApi.java | 8 ++++++++ .../api/apis/profile/usecase/MemberAccountUseCase.java | 5 +++++ .../fitapet/api/apis/test/NotificationTestController.java | 3 ++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index de23a294..7e41461a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -46,6 +46,7 @@ jobs: - name: docker image build & push run: | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} + cat ${{ secrets.IOS_FIREBASE_ADMINSDK }} > fitapet-app-external-api/src/main/resources/fitapet-ios-firebase-adminsdk-ethnn-6ec10fe329.json docker build -t ${{ secrets.DOCKER_NICKNAME }}/fitapet:latest . docker push ${{ secrets.DOCKER_NICKNAME }}/fitapet:latest diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/controller/AccountApi.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/controller/AccountApi.java index 17bcf2d0..5975566e 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/controller/AccountApi.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/controller/AccountApi.java @@ -52,6 +52,14 @@ public ResponseEntity getProfile(@PathVariable("id") Long id) { return ResponseEntity.ok(SuccessResponse.from(member)); } + @Operation(summary = "사용자 설정 이름 조회") + @Parameter(name = "id", description = "조회할 프로필 ID", in = ParameterIn.PATH, required = true) + @GetMapping("/{id}/name") + @PreAuthorize("isAuthenticated()") + public ResponseEntity getUserName(@PathVariable("id") Long id) { + return ResponseEntity.ok(SuccessResponse.from("name", memberAccountUseCase.findUserName(id))); + } + @Operation(summary = "디바이스 토큰 등록") @PostMapping("/{id}/device-token") @PreAuthorize("isAuthenticated() and #id == principal.userId") diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/usecase/MemberAccountUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/usecase/MemberAccountUseCase.java index b50763b8..9505659c 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/usecase/MemberAccountUseCase.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/profile/usecase/MemberAccountUseCase.java @@ -63,6 +63,11 @@ public AccountProfileRes getProfile(Long userId) { return AccountProfileRes.from(member); } + @Transactional(readOnly = true) + public String findUserName(Long userId) { + return memberSearchService.findById(userId).getName(); + } + @Transactional public void registerDeviceToken(Long memberId, DeviceTokenReq req) { Member member = memberSearchService.findById(memberId); diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/test/NotificationTestController.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/test/NotificationTestController.java index 83d8d015..21f19f64 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/test/NotificationTestController.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/test/NotificationTestController.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,7 +18,7 @@ public class NotificationTestController { private final NotificationTestService notificationTestService; - @RequestMapping("/push") + @GetMapping("/push") @PreAuthorize("isAuthenticated()") public void push(@AuthenticationPrincipal CustomUserDetails user) { notificationTestService.push(user.getUserId()); From 2e61bc03ef5e12f7a1e2ccbdb9a1cb4026bfb1a1 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:56:08 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20#39=20=EB=B0=98=EB=A0=A4=EB=8F=99?= =?UTF-8?q?=EB=AC=BC=20=EC=82=AD=EC=A0=9C=20&&=20Object=20Storage=20Delete?= =?UTF-8?q?=20Req=20Event=20Handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/apis/pet/controller/PetApi.java | 33 +++++++++++------ .../apis/pet/mapper/PetImageSearchMapper.java | 37 +++++++++++++++++++ .../api/apis/pet/usecase/PetUseCase.java | 22 +++++++++++ .../MemoImageQueryDslRepository.java | 7 ++++ .../MemoImageQueryDslRepositoryImpl.java | 28 ++++++++++++++ .../memo/repository/MemoImageRepository.java | 2 +- .../repository/MemoQueryDslRepository.java | 1 + .../MemoQueryDslRepositoryImpl.java | 11 ++++++ .../memo/service/MemoSearchService.java | 10 +++++ .../domain/domains/pet/dto/PetInfoRes.java | 3 +- .../domains/pet/service/PetDeleteService.java | 19 ++++++++++ .../client/s3/NcpObjectStorageService.java | 1 + .../event/ObjectStorageEventHandler.java | 29 +++++++++++++++ .../event/ObjectStorageImageDeleteEvent.java | 14 +++++++ 14 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/mapper/PetImageSearchMapper.java create mode 100644 fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepository.java create mode 100644 fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepositoryImpl.java create mode 100644 fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/service/PetDeleteService.java create mode 100644 fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageEventHandler.java create mode 100644 fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageImageDeleteEvent.java diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java index ec8a64ec..56a50dd9 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java @@ -28,7 +28,7 @@ @Tag(name = "반려동물 관리 API") @RestController -@RequestMapping("/api/v2/users/{user_id}/pets") +@RequestMapping("/api/v2/pets") @RequiredArgsConstructor @Slf4j public class PetApi { @@ -51,27 +51,36 @@ public ResponseEntity savePet(@RequestBody @Valid PetSaveReq req, @Authentica @Operation(summary = "관리 중인 반려동물 리스트 조회") @GetMapping("") - @PreAuthorize("isAuthenticated() and #userId == principal.userId") - public ResponseEntity getPets(@PathVariable("user_id") Long userId) { - PetInfoRes pets = petUseCase.getPets(userId); + @PreAuthorize("isAuthenticated()") + public ResponseEntity getPets(@AuthenticationPrincipal CustomUserDetails user) { + PetInfoRes pets = petUseCase.getPets(user.getUserId()); return ResponseEntity.ok(SuccessResponse.from("pets", pets.getPets())); } - @Operation(summary = "관리 중인 반려동물 목록 조회") - @Parameter(name = "user_id", description = "조회할 유저 ID", required = true) + @Operation(summary = "관리 중인 반려동물 요약 목록 조회") @GetMapping("/summary") - @PreAuthorize("isAuthenticated() and #userId == principal.userId") - public ResponseEntity findPets(@PathVariable("user_id") Long userId) { - List pets = petUseCase.findPetsSummaryByUserId(userId).getPets(); + @PreAuthorize("isAuthenticated()") + public ResponseEntity findPets(@AuthenticationPrincipal CustomUserDetails user) { + List pets = petUseCase.findPetsSummaryByUserId(user.getUserId()).getPets(); return ResponseEntity.ok(SuccessResponse.from("pets", pets)); } @Operation(summary = "반려동물 케어 카테고리 유효성 검사") @Parameter(name = "user_id", description = "조회할 유저 ID", in = ParameterIn.PATH, required = true) @PostMapping("/categories-check") - @PreAuthorize("isAuthenticated() and #userId == principal.userId") - public ResponseEntity checkCategoryExist(@PathVariable("user_id") Long userId, @RequestBody @Valid CareCategoryInfo.CareCategoryExistRequest request) { - List result = petUseCase.checkCategoryExist(userId, request.categoryName(), request.pets()); + @PreAuthorize("isAuthenticated()") + public ResponseEntity checkCategoryExist(@AuthenticationPrincipal CustomUserDetails user, @RequestBody @Valid CareCategoryInfo.CareCategoryExistRequest request) { + List result = petUseCase.checkCategoryExist(user.getUserId(), request.categoryName(), request.pets()); return ResponseEntity.ok(SuccessResponse.from("categories", result)); } + + @Operation(summary = "반려동물 삭제") + @Parameter(name = "pet_id", description = "삭제할 반려동물 ID", in = ParameterIn.PATH, required = true) + @DeleteMapping("/{pet_id}") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isMaster(principal.userId, #petId)") + public ResponseEntity deletePet(@PathVariable("pet_id") Long petId) { + petUseCase.deletePet(petId); + return ResponseEntity.ok(SuccessResponse.noContent()); + } + } diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/mapper/PetImageSearchMapper.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/mapper/PetImageSearchMapper.java new file mode 100644 index 00000000..cb66eef5 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/mapper/PetImageSearchMapper.java @@ -0,0 +1,37 @@ +package kr.co.fitapet.api.apis.pet.mapper; + +import kr.co.fitapet.common.annotation.Mapper; +import kr.co.fitapet.domain.domains.memo.service.MemoSearchService; +import kr.co.fitapet.domain.domains.pet.domain.Pet; +import kr.co.fitapet.domain.domains.pet.service.PetSearchService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Mapper +@RequiredArgsConstructor +public class PetImageSearchMapper { + private final PetSearchService petSearchService; + private final MemoSearchService memoSearchService; + + /** + * 반려동물 프로필 이미지와 메모에 등록된 모든 이미지 조회 + */ + @Transactional(readOnly = true) + public List findPetProfileImageAndMemoImageUrls(Long petId) { + List images = new ArrayList<>(); + + Pet pet = petSearchService.findPetById(petId); + if (pet.getPetProfileImg() != null) + images.add(pet.getPetProfileImg()); + List memoIds = memoSearchService.findMemoIdsByPetId(pet.getId()); + List memoImageUrls = memoSearchService.findMemoImageUrlsByMemoIds(memoIds); + images.addAll(memoImageUrls); + + return images; + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java index ebc086e8..6221d35a 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java @@ -1,6 +1,7 @@ package kr.co.fitapet.api.apis.pet.usecase; import kr.co.fitapet.api.apis.pet.helper.PetCareHelper; +import kr.co.fitapet.api.apis.pet.mapper.PetImageSearchMapper; import kr.co.fitapet.api.apis.pet.mapper.PetManagerMapper; import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorCode; import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorException; @@ -9,19 +10,31 @@ import kr.co.fitapet.domain.domains.memo.domain.MemoCategory; import kr.co.fitapet.domain.domains.pet.domain.Pet; import kr.co.fitapet.domain.domains.pet.dto.PetInfoRes; +import kr.co.fitapet.domain.domains.pet.service.PetDeleteService; import kr.co.fitapet.domain.domains.pet.service.PetSaveService; +import kr.co.fitapet.domain.domains.pet.service.PetSearchService; +import kr.co.fitapet.infra.common.event.ObjectStorageImageDeleteEvent; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; @UseCase +@Slf4j @RequiredArgsConstructor public class PetUseCase { private final PetManagerMapper petManagerMapper; + private final PetImageSearchMapper petImageSearchMapper; + private final PetSaveService petSaveService; + private final PetDeleteService petDeleteService; private final PetCareHelper petCareHelper; + private final ApplicationEventPublisher eventPublisher; + @Transactional public void savePet(Pet pet, Long memberId) { pet = petSaveService.savePet(pet); @@ -49,4 +62,13 @@ public PetInfoRes getPets(Long userId) { List pets = petManagerMapper.findAllManagerByMemberId(userId); return PetInfoRes.ofPetInfo(pets); } + + @Transactional + public void deletePet(Long petId) { + log.info("deletePet: {}", petId); + List deleteImageUrls = petImageSearchMapper.findPetProfileImageAndMemoImageUrls(petId); + log.info("deleteImageUrls: {}", deleteImageUrls); + petDeleteService.deletePet(petId); + eventPublisher.publishEvent(ObjectStorageImageDeleteEvent.valueOf(deleteImageUrls)); + } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepository.java new file mode 100644 index 00000000..599daec4 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepository.java @@ -0,0 +1,7 @@ +package kr.co.fitapet.domain.domains.memo.repository; + +import java.util.List; + +public interface MemoImageQueryDslRepository { + List findMemoImageUrlsByMemoIds(List memoIds); +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepositoryImpl.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepositoryImpl.java new file mode 100644 index 00000000..b8396ec5 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageQueryDslRepositoryImpl.java @@ -0,0 +1,28 @@ +package kr.co.fitapet.domain.domains.memo.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import kr.co.fitapet.domain.domains.memo.domain.QMemo; +import kr.co.fitapet.domain.domains.memo.domain.QMemoImage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class MemoImageQueryDslRepositoryImpl implements MemoImageQueryDslRepository { + private final JPAQueryFactory queryFactory; + private final QMemo memo = QMemo.memo; + private final QMemoImage memoImage = QMemoImage.memoImage; + + @Override + public List findMemoImageUrlsByMemoIds(List memoIds) { + return queryFactory + .select(memoImage.imgUrl) + .from(memoImage) + .where(memoImage.memo.id.in(memoIds)) + .fetch(); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageRepository.java index 8956ada7..eb75bba8 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoImageRepository.java @@ -8,7 +8,7 @@ import java.util.List; -public interface MemoImageRepository extends ExtendedRepository { +public interface MemoImageRepository extends ExtendedRepository, MemoImageQueryDslRepository { List findByMemo_Id(Long memoId); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("delete from MemoImage mi where mi in :memoImages") diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepository.java index bb2d6eff..43fa6b39 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepository.java @@ -8,6 +8,7 @@ import java.util.Optional; public interface MemoQueryDslRepository { + List findMemoIdsByPetId(Long petId); Optional findMemoAndMemoImageUrlsById(Long memoId); Slice findMemosInMemoCategory(Long memoCategoryId, Pageable pageable, String target); Slice findMemosByPetId(Long petId, Pageable pageable); diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepositoryImpl.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepositoryImpl.java index 9812ead0..b81fa70a 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepositoryImpl.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/repository/MemoQueryDslRepositoryImpl.java @@ -33,6 +33,17 @@ public class MemoQueryDslRepositoryImpl implements MemoQueryDslRepository { private final QMemoImage memoImage = QMemoImage.memoImage; private final QMemoCategory memoCategory = QMemoCategory.memoCategory; + @Override + public List findMemoIdsByPetId(Long petId) { + return queryFactory + .select(memo.id) + .from(memoCategory) + .leftJoin(memo).on(memo.memoCategory.id.eq(memoCategory.id)) + .where(memoCategory.pet.id.eq(petId) + .and(memo.id.isNotNull())) + .fetch(); + } + @Override public Optional findMemoAndMemoImageUrlsById(Long memoId) { List result = queryFactory diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/service/MemoSearchService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/service/MemoSearchService.java index 4dafb739..8c7e7866 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/service/MemoSearchService.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/memo/service/MemoSearchService.java @@ -56,6 +56,11 @@ public Memo findMemoById(Long memoId) { return memoRepository.findByIdOrElseThrow(memoId); } + @Transactional(readOnly = true) + public List findMemoIdsByPetId(Long petId) { + return memoRepository.findMemoIdsByPetId(petId); + } + @Transactional(readOnly = true) public MemoInfoDto.MemoInfo findMemoAndMemoImageUrlsById(Long memoId) { return memoRepository.findMemoAndMemoImageUrlsById(memoId).orElseThrow( @@ -88,4 +93,9 @@ public MemoInfoDto.PageResponse findMemosByPetIds(List petIds, Pageable pa public List findMemoImagesByMemoId(Long memoId) { return memoImageRepository.findByMemo_Id(memoId); } + + @Transactional(readOnly = true) + public List findMemoImageUrlsByMemoIds(List memoIds) { + return memoImageRepository.findMemoImageUrlsByMemoIds(memoIds); + } } \ No newline at end of file diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetInfoRes.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetInfoRes.java index 2cbd653f..28579ce1 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetInfoRes.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetInfoRes.java @@ -9,6 +9,7 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Objects; @Getter @Dto(name = "pet") @@ -36,7 +37,7 @@ private static PetSummaryInfo of(Pet pet) { return new PetSummaryInfo( pet.getId(), pet.getPetName(), - pet.getPetProfileImg() + Objects.toString(pet.getPetProfileImg(), "") ); } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/service/PetDeleteService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/service/PetDeleteService.java new file mode 100644 index 00000000..32afcc16 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/service/PetDeleteService.java @@ -0,0 +1,19 @@ +package kr.co.fitapet.domain.domains.pet.service; + +import kr.co.fitapet.common.annotation.DomainService; +import kr.co.fitapet.domain.domains.pet.repository.PetRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@DomainService +@RequiredArgsConstructor +public class PetDeleteService { + private final PetRepository petRepository; + + @Transactional + public void deletePet(Long petId) { + petRepository.deleteById(petId); + } +} diff --git a/fitapet-infra/src/main/java/kr/co/fitapet/infra/client/s3/NcpObjectStorageService.java b/fitapet-infra/src/main/java/kr/co/fitapet/infra/client/s3/NcpObjectStorageService.java index 409cf0ec..fd7d02a1 100644 --- a/fitapet-infra/src/main/java/kr/co/fitapet/infra/client/s3/NcpObjectStorageService.java +++ b/fitapet-infra/src/main/java/kr/co/fitapet/infra/client/s3/NcpObjectStorageService.java @@ -24,6 +24,7 @@ public void deleteObjects(List paths) { List nonImmutablePaths = new ArrayList<>(paths); nonImmutablePaths.replaceAll(path -> path.replace("https://" + config.getS3().bucket() + ".kr.object.ncloudstorage.com/", "")); + log.info("nonImmutablePaths: {}", nonImmutablePaths); List keys = nonImmutablePaths.stream().map(DeleteObjectsRequest.KeyVersion::new).toList(); try { diff --git a/fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageEventHandler.java b/fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageEventHandler.java new file mode 100644 index 00000000..0b26898a --- /dev/null +++ b/fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageEventHandler.java @@ -0,0 +1,29 @@ +package kr.co.fitapet.infra.common.event; + +import kr.co.fitapet.infra.client.s3.NcpObjectStorageService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ObjectStorageEventHandler { + private final NcpObjectStorageService objectStorageService; + + /** + * Object Storage에 저장된 이미지를 삭제합니다. + *
+ * {@link ObjectStorageImageDeleteEvent}를 받아서 Object Storage에 저장된 이미지를 삭제합니다. + *
+ * 해당 이벤트는 트랜잭션 내에서 실행되지 않습니다. + * @param event {@link ObjectStorageImageDeleteEvent} + */ + @TransactionalEventListener + public void handleObjectStorageImageDeleteEvent(ObjectStorageImageDeleteEvent event) { + log.info("handleObjectStorageImageDeleteEvent: {}", event); + objectStorageService.deleteObjects(event.imageUrls()); + log.info("Successfully deleted images from Object Storage"); + } +} diff --git a/fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageImageDeleteEvent.java b/fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageImageDeleteEvent.java new file mode 100644 index 00000000..55fed4a7 --- /dev/null +++ b/fitapet-infra/src/main/java/kr/co/fitapet/infra/common/event/ObjectStorageImageDeleteEvent.java @@ -0,0 +1,14 @@ +package kr.co.fitapet.infra.common.event; + +import java.util.List; + +/** + * Object Storage Image Delete Event를 위한 Object + */ +public record ObjectStorageImageDeleteEvent( + List imageUrls +) { + public static ObjectStorageImageDeleteEvent valueOf(List imageUrls) { + return new ObjectStorageImageDeleteEvent(imageUrls); + } +} From db3c5db3e21879b31d9cd3565b304a931cc77ffe Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:53:58 +0900 Subject: [PATCH 3/5] feaat: #105 pet update && pet detail info receive api --- .../api/apis/pet/controller/PetApi.java | 24 ++++++++++--- .../api/apis/pet/dto/PetDetailRes.java | 36 +++++++++++++++++++ .../api/apis/pet/usecase/PetUseCase.java | 24 ++++++++++--- .../domain/domains/pet/domain/Pet.java | 10 ++++++ .../domain/domains/pet/dto/PetSaveReq.java | 5 ++- 5 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/dto/PetDetailRes.java diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java index 56a50dd9..a9602ea1 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/controller/PetApi.java @@ -44,7 +44,7 @@ public class PetApi { @PostMapping("") @PreAuthorize("isAuthenticated()") public ResponseEntity savePet(@RequestBody @Valid PetSaveReq req, @AuthenticationPrincipal CustomUserDetails user) { - petUseCase.savePet(req.toPetEntity(), user.getUserId()); + petUseCase.savePet(req.toEntity(), user.getUserId()); return ResponseEntity.ok(SuccessResponse.noContent()); } @@ -53,7 +53,7 @@ public ResponseEntity savePet(@RequestBody @Valid PetSaveReq req, @Authentica @GetMapping("") @PreAuthorize("isAuthenticated()") public ResponseEntity getPets(@AuthenticationPrincipal CustomUserDetails user) { - PetInfoRes pets = petUseCase.getPets(user.getUserId()); + PetInfoRes pets = petUseCase.findPets(user.getUserId()); return ResponseEntity.ok(SuccessResponse.from("pets", pets.getPets())); } @@ -65,6 +65,13 @@ public ResponseEntity findPets(@AuthenticationPrincipal CustomUserDetails use return ResponseEntity.ok(SuccessResponse.from("pets", pets)); } + @Operation(summary = "반려동물 정보 조회") + @GetMapping("/{pet_id}") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isManager(principal.userId, #petId)") + public ResponseEntity getPet(@PathVariable("pet_id") Long petId) { + return ResponseEntity.ok(SuccessResponse.from("pet", petUseCase.findPet(petId))); + } + @Operation(summary = "반려동물 케어 카테고리 유효성 검사") @Parameter(name = "user_id", description = "조회할 유저 ID", in = ParameterIn.PATH, required = true) @PostMapping("/categories-check") @@ -74,12 +81,21 @@ public ResponseEntity checkCategoryExist(@AuthenticationPrincipal CustomUserD return ResponseEntity.ok(SuccessResponse.from("categories", result)); } + @Operation(summary = "반려동물 정보 수정") + @Parameter(name = "pet_id", description = "수정할 반려동물 ID", in = ParameterIn.PATH, required = true) + @PutMapping("/{pet_id}") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isMaster(principal.userId, #petId)") + public ResponseEntity updatePet(@PathVariable("pet_id") Long petId, @RequestBody @Valid PetSaveReq req) { + petUseCase.updatePet(petId, req); + return ResponseEntity.ok(SuccessResponse.noContent()); + } + @Operation(summary = "반려동물 삭제") @Parameter(name = "pet_id", description = "삭제할 반려동물 ID", in = ParameterIn.PATH, required = true) @DeleteMapping("/{pet_id}") @PreAuthorize("isAuthenticated() and @managerAuthorize.isMaster(principal.userId, #petId)") - public ResponseEntity deletePet(@PathVariable("pet_id") Long petId) { - petUseCase.deletePet(petId); + public ResponseEntity deletePet(@PathVariable("pet_id") Long petId, @AuthenticationPrincipal CustomUserDetails user) { + petUseCase.deletePet(petId, user.getUserId()); return ResponseEntity.ok(SuccessResponse.noContent()); } diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/dto/PetDetailRes.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/dto/PetDetailRes.java new file mode 100644 index 00000000..22835443 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/dto/PetDetailRes.java @@ -0,0 +1,36 @@ +package kr.co.fitapet.api.apis.pet.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import kr.co.fitapet.domain.domains.pet.domain.Pet; +import kr.co.fitapet.domain.domains.pet.type.GenderType; + +import java.time.LocalDate; +import java.util.Objects; + +public record PetDetailRes( + Long id, + String petName, + String petProfileImage, + GenderType gender, + Boolean neutered, + @JsonSerialize(using = LocalDateSerializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") + LocalDate birthdate, + String species, + String feed +) { + public static PetDetailRes from(Pet pet) { + return new PetDetailRes( + pet.getId(), + pet.getPetName(), + Objects.toString(pet.getPetProfileImg(), ""), + pet.getGender(), + pet.isNeutered(), + pet.getBirthdate(), + pet.getSpecies(), + Objects.toString(pet.getFeed(), "") + ); + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java index 6221d35a..ec50125f 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/usecase/PetUseCase.java @@ -1,25 +1,26 @@ package kr.co.fitapet.api.apis.pet.usecase; +import kr.co.fitapet.api.apis.pet.dto.PetDetailRes; import kr.co.fitapet.api.apis.pet.helper.PetCareHelper; import kr.co.fitapet.api.apis.pet.mapper.PetImageSearchMapper; import kr.co.fitapet.api.apis.pet.mapper.PetManagerMapper; import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorCode; import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorException; import kr.co.fitapet.common.annotation.UseCase; -import kr.co.fitapet.domain.domains.care.service.CareSearchService; import kr.co.fitapet.domain.domains.memo.domain.MemoCategory; import kr.co.fitapet.domain.domains.pet.domain.Pet; import kr.co.fitapet.domain.domains.pet.dto.PetInfoRes; +import kr.co.fitapet.domain.domains.pet.dto.PetSaveReq; import kr.co.fitapet.domain.domains.pet.service.PetDeleteService; import kr.co.fitapet.domain.domains.pet.service.PetSaveService; import kr.co.fitapet.domain.domains.pet.service.PetSearchService; import kr.co.fitapet.infra.common.event.ObjectStorageImageDeleteEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; @UseCase @@ -29,6 +30,7 @@ public class PetUseCase { private final PetManagerMapper petManagerMapper; private final PetImageSearchMapper petImageSearchMapper; + private final PetSearchService petSearchService; private final PetSaveService petSaveService; private final PetDeleteService petDeleteService; private final PetCareHelper petCareHelper; @@ -58,13 +60,27 @@ public List checkCategoryExist(Long userId, String categoryName, List p } @Transactional(readOnly = true) - public PetInfoRes getPets(Long userId) { + public PetInfoRes findPets(Long userId) { List pets = petManagerMapper.findAllManagerByMemberId(userId); return PetInfoRes.ofPetInfo(pets); } + @Transactional(readOnly = true) + public PetDetailRes findPet(Long petId) { + Pet pet = petSearchService.findPetById(petId); + return PetDetailRes.from(pet); + } + + @Transactional + public void updatePet(Long petId, PetSaveReq req) { + Pet pet = petSearchService.findPetById(petId); + pet.updatePet(req.toEntity()); + petSaveService.savePet(pet); + } + @Transactional - public void deletePet(Long petId) { + @CacheEvict(value = "master", key = "#masterId + '@' + #petId", cacheManager = "managerCacheManager") + public void deletePet(Long petId, Long masterId) { log.info("deletePet: {}", petId); List deleteImageUrls = petImageSearchMapper.findPetProfileImageAndMemoImageUrls(petId); log.info("deleteImageUrls: {}", deleteImageUrls); diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/domain/Pet.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/domain/Pet.java index dc837b92..6824703c 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/domain/Pet.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/domain/Pet.java @@ -70,4 +70,14 @@ public static Pet of(String petName, GenderType gender, String petProfileImg, bo .species(species) .feed(feed).build(); } + + public void updatePet(Pet pet) { + this.petName = pet.getPetName(); + this.species = pet.getSpecies(); + this.gender = pet.getGender(); + this.neutered = pet.isNeutered(); + this.birthdate = pet.getBirthdate(); + this.petProfileImg = pet.getPetProfileImg(); + this.feed = pet.getFeed(); + } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetSaveReq.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetSaveReq.java index 82d504ee..90ae8039 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetSaveReq.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/dto/PetSaveReq.java @@ -31,8 +31,10 @@ public class PetSaveReq { private LocalDate birthdate; @Schema(description = "반려동물 프로필 이미지", nullable = true) private String profileImg; + @Schema(description = "반려동물 사료 정보", nullable = true) + private String feed; - public Pet toPetEntity() { + public Pet toEntity() { return Pet.builder() .petName(petName) .species(species) @@ -40,6 +42,7 @@ public Pet toPetEntity() { .neutered(neutralization) .birthdate(birthdate) .petProfileImg(StringUtils.hasText(profileImg) ? profileImg : null) + .feed(StringUtils.hasText(feed) ? feed : null) .build(); } } From 20bcee5210ac5625f823858e18ee6e9df26e12a7 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:09:00 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20#120=20=EC=B4=88=EB=8C=80=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=9C=A0=EC=A0=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ManagerInvitationRepositoryImpl.java | 1 + .../manager/ManagerInvitationServiceImpl.java | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepositoryImpl.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepositoryImpl.java index b9c37a4e..19b2a0db 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepositoryImpl.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepositoryImpl.java @@ -43,4 +43,5 @@ public Map findAll(String petId) { public void delete(String petId, Long invitedId) { ops.delete(KEY + ":" + petId, invitedId); } + } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationServiceImpl.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationServiceImpl.java index ef766f5a..8468a764 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationServiceImpl.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationServiceImpl.java @@ -33,7 +33,18 @@ public void save(Long invitedId, Long petId) { @Override public List findAll(Long petId) { Map all = managerInvitationRepository.findAll(petId.toString()); + + all.forEach((k, v) -> { + boolean expired = v.isBefore(LocalDateTime.now()); + + if (expired) { + log.warn("manager invitation expired. about User : {}", k); + delete(k, petId); + } + }); + return all.entrySet().stream() + .filter(entry -> !entry.getValue().isBefore(LocalDateTime.now())) .map(entry -> InvitationDto.of(petId, entry.getKey(), entry.getValue())) .toList(); } @@ -44,7 +55,14 @@ public boolean expired(Long invitedId, Long petId) { LocalDateTime ttl = managerInvitationRepository.getTtl(petId.toString(), invitedId); log.info("manager invitation ttl : {}", ttl); - return ttl.isBefore(LocalDateTime.now()); + boolean expired = ttl.isBefore(LocalDateTime.now()); + + if (expired) { + log.warn("manager invitation expired. about User : {}", invitedId); + delete(invitedId, petId); + return true; + } + return false; } @Override From 0a3ac58c146ac0e66f313b1cd568500c4de14d4e Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:44:17 +0900 Subject: [PATCH 5/5] feat: #120 notification api --- .../notification/controller/NotificationApi.java | 15 +++++++++++++++ .../notification/usecase/NotificationUseCase.java | 11 +++++++++++ 2 files changed, 26 insertions(+) create mode 100644 fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/controller/NotificationApi.java create mode 100644 fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/usecase/NotificationUseCase.java diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/controller/NotificationApi.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/controller/NotificationApi.java new file mode 100644 index 00000000..7fcd7e11 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/controller/NotificationApi.java @@ -0,0 +1,15 @@ +package kr.co.fitapet.api.apis.notification.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "알림 API") +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/notifications") +public class NotificationApi { +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/usecase/NotificationUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/usecase/NotificationUseCase.java new file mode 100644 index 00000000..716e8078 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/notification/usecase/NotificationUseCase.java @@ -0,0 +1,11 @@ +package kr.co.fitapet.api.apis.notification.usecase; + +import kr.co.fitapet.common.annotation.UseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@UseCase +@RequiredArgsConstructor +public class NotificationUseCase { +}