diff --git a/.github/workflows/kakao_ci.yml b/.github/workflows/kakao_ci.yml index 17a79fac..2e7ab725 100644 --- a/.github/workflows/kakao_ci.yml +++ b/.github/workflows/kakao_ci.yml @@ -76,7 +76,7 @@ jobs: - name: run kakao-chat at Windows if: runner.os == 'Windows' - uses: psychology50/kakao-chat-ci@v1.6 + uses: psychology50/kakao-chat-ci@v1.8 env: KAKAO_CLIENT: ${{ secrets.KAKAO_CLIENT }} KAKAO_EMAIL: ${{ secrets.KAKAO_EMAIL }} @@ -90,7 +90,7 @@ jobs: - name: run kakao-chat at macOS if: runner.os == 'macOS' - uses: psychology50/kakao-chat-ci@v1.6 + uses: psychology50/kakao-chat-ci@v1.8 env: KAKAO_CLIENT: ${{ secrets.KAKAO_CLIENT }} KAKAO_EMAIL: ${{ secrets.KAKAO_EMAIL }} diff --git a/README.md b/README.md index 7213af4c..95a71e39 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@ - [Branch Convention](#branch-convention) ## Version Control -| Version # | Revision Date | Description | Author | -|:---------:|:-------------:|:--------------------|:------:| -| v0.0.1 | 2023.10.1 | 프로젝트 기본 기능 구현 및 배포 | 양재서 | +| Version # | Revision Date | Description | Author | +|:---------:|:-------------:|:-------------------|:------:| +| v0.0.1 | 2023.10.1 | 프로젝트 기본 기능 구현 및 배포 | 양재서 | +| v0.0.2 | 2024.02.17 | 멀티 모듈화 아키텍처 추가 | 양재서 | ## Dev Environment - IntelliJ 2023.1.2 @@ -81,7 +82,7 @@ - [ ] 발견되는 버그와 개선사항들을 정리하고 쌓인 이슈들을 체계적으로 관리해보았다. - [X] 코드를 지속적으로 리팩토링하고 디자인 패턴을 적용해보았다. - [X] 위의 시도에서 더 좋은 설계와 더 빠른 개발 사이의 트레이드 오프를 고민해본 적이 있다. -- [ ] 반복되는 수정과 배포에 수반되는 작업들을 자동화 해보았다. +- [X] 반복되는 수정과 배포에 수반되는 작업들을 자동화 해보았다. - [ ] 언어나 프레임워크만으로 구현할 수 없는 것들을 직접 구현해보았다. - [ ] 내가 사용한 라이브러리나 프레임 워크의 한계를 느끼고 개선해보았다. - [ ] 코드나 제품의 퀄리티를 유지하기 위한 분석툴이나 테스트 툴을 도입해보았다. diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/controller/AuthApi.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/controller/AuthApi.java index 2db21a11..2f3c2c8b 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/controller/AuthApi.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/controller/AuthApi.java @@ -28,7 +28,7 @@ import kr.co.fitapet.api.common.util.cookie.CookieUtil; import kr.co.fitapet.common.execption.GlobalErrorException; import kr.co.fitapet.domain.common.redis.sms.type.SmsPrefix; -import kr.co.fitapet.domain.domains.member.domain.AccessToken; +import kr.co.fitapet.domain.common.redis.AccessToken; import kr.co.fitapet.infra.client.sms.snes.exception.SmsErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/mapper/JwtMapper.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/mapper/JwtMapper.java index 8fd533cc..ff9cc209 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/mapper/JwtMapper.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/mapper/JwtMapper.java @@ -11,15 +11,13 @@ import kr.co.fitapet.domain.common.redis.forbidden.ForbiddenTokenService; import kr.co.fitapet.domain.common.redis.refresh.RefreshToken; import kr.co.fitapet.domain.common.redis.refresh.RefreshTokenService; -import kr.co.fitapet.domain.domains.member.domain.AccessToken; +import kr.co.fitapet.domain.common.redis.AccessToken; import org.springframework.util.StringUtils; import java.time.Duration; import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.Map; -import static java.util.Calendar.ZONE_OFFSET; import static kr.co.fitapet.api.common.security.jwt.consts.JwtType.*; @Mapper diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/usecase/MemberAuthUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/usecase/MemberAuthUseCase.java index 659ace40..80d7364e 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/usecase/MemberAuthUseCase.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/auth/usecase/MemberAuthUseCase.java @@ -17,7 +17,7 @@ import kr.co.fitapet.common.execption.BaseErrorCode; import kr.co.fitapet.common.execption.GlobalErrorException; import kr.co.fitapet.domain.common.redis.sms.type.SmsPrefix; -import kr.co.fitapet.domain.domains.member.domain.AccessToken; +import kr.co.fitapet.domain.common.redis.AccessToken; import kr.co.fitapet.domain.domains.member.domain.Member; import kr.co.fitapet.domain.domains.member.exception.AccountErrorCode; import kr.co.fitapet.domain.domains.member.service.MemberSaveService; diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/care/usecase/CareUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/care/usecase/CareUseCase.java index be055604..c8926236 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/care/usecase/CareUseCase.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/care/usecase/CareUseCase.java @@ -17,6 +17,7 @@ import kr.co.fitapet.domain.domains.care_log.dto.CareLogInfo; import kr.co.fitapet.domain.domains.care_log.service.CareLogSearchService; import kr.co.fitapet.domain.domains.care_log.service.CareLogUpdateService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; import kr.co.fitapet.domain.domains.member.service.MemberSearchService; import kr.co.fitapet.domain.domains.pet.domain.Pet; import kr.co.fitapet.domain.domains.pet.exception.PetErrorCode; @@ -36,6 +37,7 @@ public class CareUseCase { private final CareUpdateService careUpdateService; private final MemberSearchService memberSearchService; + private final ManagerSearchService managerSearchService; private final PetSearchService petSearchService; private final CareSearchService careSearchService; private final CareLogSearchService careLogSearchService; @@ -105,7 +107,7 @@ public void saveCare(Long userId, Long petId, CareSaveReq.Request request) { List additionalPetDtos = request.pets(); List petIds = additionalPetDtos.stream().map(CareSaveReq.AdditionalPetDto::petId).toList(); - if (!memberSearchService.isManagerAll(userId, petIds)) { + if (!managerSearchService.isManagerAll(userId, petIds)) { throw new GlobalErrorException(PetErrorCode.NOT_MANAGER_PET); } diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/controller/MangerApi.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/controller/MangerApi.java new file mode 100644 index 00000000..c5325288 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/controller/MangerApi.java @@ -0,0 +1,114 @@ +package kr.co.fitapet.api.apis.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import kr.co.fitapet.api.apis.manager.dto.InviteMemberReq; +import kr.co.fitapet.api.apis.manager.usecase.ManagerUseCase; +import kr.co.fitapet.api.common.response.SuccessResponse; +import kr.co.fitapet.api.common.security.authentication.CustomUserDetails; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "매니저 API", description = "반려동물 관리자 및 어드민의 기능을 제공하는 API") +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/pets/{pet_id}/managers") +public class MangerApi { + private final ManagerUseCase managerUseCase; + + @Operation(summary = "매니저 목록 조회") + @Parameter(name = "pet_id", description = "반려동물 ID", in = ParameterIn.PATH, required = true) + @GetMapping("") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isManager(principal.userId, #petId)") + public ResponseEntity getManagers(@PathVariable("pet_id") Long petId, @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok(SuccessResponse.from("managers", managerUseCase.findManagers(petId, userDetails.getUserId()))); + } + + @Operation(summary = "반려동물 관리자 초대 리스트 조회") + @Parameter(name = "pet_id", description = "반려동물 ID", in = ParameterIn.PATH, required = true) + @GetMapping("/invite") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isManager(principal.userId, #petId)") + public ResponseEntity getInvitedMembers(@PathVariable("pet_id") Long petId, @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok(SuccessResponse.from("members", managerUseCase.findInvitedMembers(petId, userDetails.getUserId()))); + } + + // TODO: 2024-02-17 초대 요청 시 해당 유저에게 PUSH 알림을 전송해야 함 + @Operation(summary = "매니저 초대", description = "요청자와 유저 아이디가 동일한 경우 에러 응답을 반환합니다. 초대 요청에 대한 승인 유효 기간은 1일입니다.") + @Parameter(name = "pet_id", description = "반려동물 ID", in = ParameterIn.PATH, required = true) + @PostMapping("/invite") + @PreAuthorize("isAuthenticated() and not #req.inviteId().equals(principal.userId) and @managerAuthorize.isManager(principal.userId, #petId)") + public ResponseEntity inviteManager(@PathVariable("pet_id") Long petId, @RequestBody @Valid InviteMemberReq req, @AuthenticationPrincipal CustomUserDetails principal) + { + managerUseCase.invite(petId, req.inviteId()); + return ResponseEntity.ok(SuccessResponse.noContent()); + } + + @Operation(summary = "매니저 초대 승인", description = "매니저 초대 요청에 대한 승인을 진행합니다. 1일 이내에 승인하지 않으면 초대 요청이 만료됩니다.") + @Parameter(name = "pet_id", description = "반려동물 ID", in = ParameterIn.PATH, required = true) + @PutMapping("/invite") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isInvitedMember(principal.userId, #petId)") + public ResponseEntity agreeInvite(@PathVariable("pet_id") Long petId, @AuthenticationPrincipal CustomUserDetails userDetails) { + managerUseCase.agreeInvite(petId, userDetails.getUserId()); + return ResponseEntity.ok(SuccessResponse.noContent()); + } + + @Operation(summary = "매니저 초대 취소/거절", description = "매니저 초대 요청에 대한 취소 또는 거절을 진행합니다.") + @Parameters({ + @Parameter(name = "pet_id", description = "반려동물 ID", in = ParameterIn.PATH, required = true), + @Parameter(name = "id", description = "초대를 취소/거부할 유저 ID", in = ParameterIn.QUERY, required = true) + }) + @DeleteMapping("/invite") + @PreAuthorize("isAuthenticated() and (@managerAuthorize.isManager(principal.userId, #petId) or @managerAuthorize.isInvitedMember(principal.userId, #petId))") + public ResponseEntity cancelInvite(@PathVariable("pet_id") Long petId, @RequestParam("id") Long invitedId) { + managerUseCase.cancelInvite(petId, invitedId); + return ResponseEntity.ok(SuccessResponse.noContent()); + } + + @Operation(summary = "마스터 위임", description = "마스터 권한을 다른 매니저에게 위임합니다.") + @Parameters({ + @Parameter(name = "pet_id", description = "반려동물 ID", in = ParameterIn.PATH, required = true), + @Parameter(name = "manager_id", description = "위임할 매니저 ID", in = ParameterIn.PATH, required = true) + }) + @PatchMapping("/{manager_id}") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isMaster(principal.userId, #petId) and @managerAuthorize.isManager(#managerId, #petId)") + public ResponseEntity delegateMaster( + @PathVariable("pet_id") Long petId, + @PathVariable("manager_id") Long managerId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + managerUseCase.delegateMaster(userDetails.getUserId(), managerId, petId); + return ResponseEntity.ok(SuccessResponse.noContent()); + } + + @Operation(summary = "매니저 탈퇴/추방", description = """ + 매니저가 반려동물을 탈퇴하거나 마스터가 추방하는 기능을 제공합니다.
+ 매니저가 반려동물을 탈퇴하면 해당 반려동물의 매니저 목록에서 제거됩니다.
+ 요청자와 매니저가 동일하지 않은 경우, 마스터 권한이 있는 경우에만 요청이 성공합니다.
+ 요청자와 매니저가 동일하지만 요청자가 마스터인 경우 요청이 실패합니다. (마스터가 탈퇴하려면 반드시 다른 관리자에게 위임하거나, 반려동물 삭제 API를 사용해야 합니다.) + """) + @Parameters({ + @Parameter(name = "pet_id", description = "반려동물 ID", in = ParameterIn.PATH, required = true), + @Parameter(name = "manager_id", description = "매니저 ID", in = ParameterIn.PATH, required = true) + }) + @DeleteMapping("/{manager_id}") + @PreAuthorize("isAuthenticated() and @managerAuthorize.isManager(#managerId, #petId) " + + "and ((@managerAuthorize.isMaster(principal.userId, #petId) and not #managerId.equals(principal.userId)) " + + "or (not @managerAuthorize.isMaster(principal.userId, #petId) and @managerAuthorize.isManager(principal.userId, #petId) and #managerId.equals(principal.userId)))") + public ResponseEntity deleteManager( + @PathVariable("pet_id") Long petId, + @PathVariable("manager_id") Long managerId, + @AuthenticationPrincipal CustomUserDetails userDetails + ) { + managerUseCase.expelManager(managerId, petId); + return ResponseEntity.ok(SuccessResponse.noContent()); + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberInfoRes.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberInfoRes.java new file mode 100644 index 00000000..8363c93c --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberInfoRes.java @@ -0,0 +1,40 @@ +package kr.co.fitapet.api.apis.manager.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import kr.co.fitapet.domain.domains.member.dto.MemberInfo; +import lombok.Builder; + +import java.time.LocalDateTime; + +@Builder +@Schema(description = "매니저 초대 정보 응답") +public record InviteMemberInfoRes( + @Schema(description = "유저 ID", example = "1") + Long id, + @Schema(description = "유저 UID", example = "fitapet") + String uid, + @Schema(description = "유저 이름", example = "피타펫") + String name, + @Schema(description = "유저 프로필 이미지 URL", example = "https://fitapet.co.kr/fitapet.png") + String profileImageUrl, + @Schema(description = "초대 일시", example = "2021-08-01 00:00:00") + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + LocalDateTime invitedAt, + @Schema(description = "초대 만료 여부", example = "false") + Boolean expired +) { + public static InviteMemberInfoRes valueOf(MemberInfo member, LocalDateTime invitedAt, Boolean expired) { + return InviteMemberInfoRes.builder() + .id(member.id()) + .uid(member.uid()) + .name(member.name()) + .profileImageUrl(member.profileImageUrl()) + .invitedAt(invitedAt) + .expired(expired) + .build(); + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberReq.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberReq.java new file mode 100644 index 00000000..aae1b59d --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberReq.java @@ -0,0 +1,9 @@ +package kr.co.fitapet.api.apis.manager.dto; + +import jakarta.validation.constraints.NotNull; + +public record InviteMemberReq( + @NotNull + Long inviteId +) { +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/mapper/ManagerInvitationMapper.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/mapper/ManagerInvitationMapper.java new file mode 100644 index 00000000..3d4d6ae7 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/mapper/ManagerInvitationMapper.java @@ -0,0 +1,60 @@ +package kr.co.fitapet.api.apis.manager.mapper; + +import kr.co.fitapet.api.apis.manager.dto.InviteMemberInfoRes; +import kr.co.fitapet.common.annotation.Mapper; +import kr.co.fitapet.domain.common.redis.manager.InvitationDto; +import kr.co.fitapet.domain.common.redis.manager.ManagerInvitationService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSaveService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; +import kr.co.fitapet.domain.domains.manager.type.ManageType; +import kr.co.fitapet.domain.domains.member.domain.Member; +import kr.co.fitapet.domain.domains.member.dto.MemberInfo; +import kr.co.fitapet.domain.domains.member.exception.AccountErrorCode; +import kr.co.fitapet.domain.domains.member.exception.AccountErrorException; +import kr.co.fitapet.domain.domains.member.service.MemberSearchService; +import kr.co.fitapet.domain.domains.pet.domain.Pet; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Mapper +@RequiredArgsConstructor +public class ManagerInvitationMapper { + private final MemberSearchService memberSearchService; + private final ManagerSearchService managerSearchService; + private final ManagerSaveService managerSaveService; + private final ManagerInvitationService managerInvitationService; + + @Transactional(readOnly = true) + public void invite(Long petId, Long invitedId) { + if (!memberSearchService.isExistById(invitedId)) + throw new AccountErrorException(AccountErrorCode.NOT_FOUND_MEMBER_ERROR); + if (managerSearchService.isManager(invitedId, petId)) + throw new AccountErrorException(AccountErrorCode.ALREADY_MANAGER_ERROR); + managerInvitationService.save(invitedId, petId); + } + + @Transactional(readOnly = true) + public List findInvitedMembers(Long petId, Long requesterId) { + List invitationDtos = managerInvitationService.findAll(petId); + List members = memberSearchService.findMemberInfos(invitationDtos.stream().map(InvitationDto::inviteId).toList(), requesterId); + return members.stream().map(member -> { + InvitationDto invitationDto = invitationDtos.stream().filter(dto -> dto.inviteId().equals(member.id())).findFirst().orElseThrow(); + return InviteMemberInfoRes.valueOf(member, invitationDto.ttl(), invitationDto.expired()); + }).toList(); + } + + @Transactional + public void addManager(Long memberId, Pet pet) { + Member member = memberSearchService.findById(memberId); + managerSaveService.mappingMemberAndPet(member, pet, ManageType.MANAGER); + managerInvitationService.delete(memberId, pet.getId()); + } + + public void cancel(Long petId, Long memberId) { + managerInvitationService.delete(memberId, petId); + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/usecase/ManagerUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/usecase/ManagerUseCase.java new file mode 100644 index 00000000..bc6a9ccd --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/usecase/ManagerUseCase.java @@ -0,0 +1,70 @@ +package kr.co.fitapet.api.apis.manager.usecase; + +import kr.co.fitapet.api.apis.manager.dto.InviteMemberInfoRes; +import kr.co.fitapet.api.apis.manager.mapper.ManagerInvitationMapper; +import kr.co.fitapet.common.annotation.UseCase; +import kr.co.fitapet.domain.domains.manager.domain.Manager; +import kr.co.fitapet.domain.domains.manager.dto.ManagerInfoRes; +import kr.co.fitapet.domain.domains.manager.service.ManagerDeleteService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSaveService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; +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.cache.annotation.CacheEvict; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@UseCase +@RequiredArgsConstructor +public class ManagerUseCase { + private final ManagerSearchService managerSearchService; + private final ManagerDeleteService managerDeleteService; + + private final PetSearchService petSearchService; + + private final ManagerInvitationMapper managerInvitationMapper; + + @Transactional(readOnly = true) + public List findManagers(Long petId, Long memberId) { + return managerSearchService.findAllByPetId(petId, memberId); + } + + @Transactional(readOnly = true) + public void invite(Long petId, Long invitedId) { + managerInvitationMapper.invite(petId, invitedId); + } + + @Transactional(readOnly = true) + public List findInvitedMembers(Long petId, Long requesterId) { + return managerInvitationMapper.findInvitedMembers(petId, requesterId); + } + + @Transactional + public void agreeInvite(Long petId, Long memberId) { + Pet pet = petSearchService.findPetById(petId); + managerInvitationMapper.addManager(memberId, pet); + } + + public void cancelInvite(Long petId, Long memberId) { + managerInvitationMapper.cancel(petId, memberId); + } + + @Transactional + @CacheEvict(value = "master", key = "#masterId + '@' + #petId", cacheManager = "managerCacheManager") + public void delegateMaster(Long masterId, Long managerId, Long petId) { + Manager master = managerSearchService.findByMemberIdAndPetId(masterId, petId); + Manager manager = managerSearchService.findByMemberIdAndPetId(managerId, petId); + + master.delegateMaster(manager); + } + + @Transactional + public void expelManager(Long targetId, Long petId) { + Manager manager = managerSearchService.findByMemberIdAndPetId(targetId, petId); + managerDeleteService.deleteManager(manager); + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/mapper/PetManagerMapper.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/mapper/PetManagerMapper.java new file mode 100644 index 00000000..67269a50 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/pet/mapper/PetManagerMapper.java @@ -0,0 +1,44 @@ +package kr.co.fitapet.api.apis.pet.mapper; + +import kr.co.fitapet.common.annotation.Mapper; +import kr.co.fitapet.domain.domains.manager.domain.Manager; +import kr.co.fitapet.domain.domains.manager.service.ManagerSaveService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; +import kr.co.fitapet.domain.domains.member.service.MemberSearchService; +import kr.co.fitapet.domain.domains.manager.type.ManageType; +import kr.co.fitapet.domain.domains.pet.domain.Pet; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Mapper +@Slf4j +@RequiredArgsConstructor +public class PetManagerMapper { + private final MemberSearchService memberSearchService; + private final ManagerSearchService managerSearchService; + private final ManagerSaveService managerSaveService; + + @Transactional + public void mappingMemberAndPet(Long memberId, Pet pet) { + managerSaveService.mappingMemberAndPet(memberSearchService.findById(memberId), pet, ManageType.MASTER); + } + + @Transactional(readOnly = true) + public List findAllPetByMemberId(Long memberId) { + return managerSearchService.findAllByMemberId(memberId).stream().map(Manager::getPet).toList(); + } + + @Transactional(readOnly = true) + public boolean isManagerAll(Long memberId, List petIds) { + return managerSearchService.isManagerAll(memberId, petIds); + } + + @Transactional(readOnly = true) + public List findAllManagerByMemberId(Long memberId) { + return managerSearchService.findAllByMemberId(memberId) + .stream().map(Manager::getPet).toList(); + } +} 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 b5b80c3d..9ec67a82 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,19 +1,14 @@ package kr.co.fitapet.api.apis.pet.usecase; +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.member.domain.Manager; -import kr.co.fitapet.domain.domains.member.domain.Member; -import kr.co.fitapet.domain.domains.member.service.MemberSaveService; -import kr.co.fitapet.domain.domains.member.service.MemberSearchService; -import kr.co.fitapet.domain.domains.member.type.ManageType; 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.PetSaveService; -import kr.co.fitapet.domain.domains.pet.service.PetSearchService; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; @@ -22,10 +17,8 @@ @UseCase @RequiredArgsConstructor public class PetUseCase { - private final MemberSearchService memberSearchService; - private final MemberSaveService memberSaveService; + private final PetManagerMapper petManagerMapper; private final PetSaveService petSaveService; - private final PetSearchService petSearchService; private final CareSearchService careSearchService; @Transactional @@ -33,20 +26,18 @@ public void savePet(Pet pet, Long memberId) { pet = petSaveService.savePet(pet); MemoCategory.ofRootInstance(pet.getPetName(), pet); - - Member member = memberSearchService.findById(memberId); - memberSaveService.mappingMemberAndPet(member, pet, ManageType.MASTER); + petManagerMapper.mappingMemberAndPet(memberId, pet); } @Transactional(readOnly = true) public PetInfoRes findPetsSummaryByUserId(Long userId) { - List pets = memberSearchService.findAllManagerByMemberId(userId).stream().map(Manager::getPet).toList(); + List pets = petManagerMapper.findAllPetByMemberId(userId); return PetInfoRes.ofSummary(pets); } @Transactional(readOnly = true) public List checkCategoryExist(Long userId, String categoryName, List petIds) { - if (!memberSearchService.isManagerAll(userId, petIds)) + if (!petManagerMapper.isManagerAll(userId, petIds)) throw new AuthErrorException(AuthErrorCode.FORBIDDEN_ACCESS_TOKEN, "관리자 권한이 없습니다."); return careSearchService.checkCategoryExist(categoryName, petIds); @@ -54,8 +45,7 @@ public List checkCategoryExist(Long userId, String categoryName, List p @Transactional(readOnly = true) public PetInfoRes getPets(Long userId) { - List managers = memberSearchService.findAllManagerByMemberId(userId); - List pets = managers.stream().map(Manager::getPet).toList(); + List pets = petManagerMapper.findAllManagerByMemberId(userId); return PetInfoRes.ofPetInfo(pets); } } 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 8430775a..6c351f06 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 @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import kr.co.fitapet.api.apis.profile.usecase.MemberAccountUseCase; import kr.co.fitapet.api.common.response.SuccessResponse; import kr.co.fitapet.api.common.security.authentication.CustomUserDetails; @@ -14,6 +15,7 @@ import kr.co.fitapet.domain.domains.member.dto.AccountProfileRes; import kr.co.fitapet.api.apis.profile.dto.AccountSearchReq; import kr.co.fitapet.api.apis.profile.dto.ProfilePatchReq; +import kr.co.fitapet.domain.domains.member.dto.MemberNicknamePutReq; import kr.co.fitapet.domain.domains.member.dto.UidRes; import kr.co.fitapet.domain.domains.member.type.MemberAttrType; import kr.co.fitapet.domain.domains.notification.type.NotificationType; @@ -22,6 +24,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -45,6 +48,14 @@ public ResponseEntity getProfile(@PathVariable("id") Long id) { return ResponseEntity.ok(SuccessResponse.from(member)); } + @Operation(summary = "프로필 검색") + @Parameter(name = "search", description = "검색할 닉네임", in = ParameterIn.QUERY, required = true) + @GetMapping("") + @PreAuthorize("isAuthenticated()") + public ResponseEntity getSearchProfile(@RequestParam("search") @NotBlank String search, @AuthenticationPrincipal CustomUserDetails userDetails) { + return ResponseEntity.ok(SuccessResponse.from(memberAccountUseCase.searchProfile(userDetails.getUserId(), search))); + } + @Operation(summary = "닉네임 존재 확인") @Parameter(name = "uid", description = "확인할 유저 닉네임", in = ParameterIn.QUERY, required = true) @GetMapping("/exists") @@ -103,11 +114,21 @@ public ResponseEntity postSearchIdOrPassword( public ResponseEntity putNotify( @PathVariable Long id, @AuthenticationPrincipal CustomUserDetails user, - @RequestParam("type") @NotBlank NotificationType type) { + @RequestParam("type") @NotBlank NotificationType type + ) { memberAccountUseCase.updateNotification(id, user.getUserId(), type); return ResponseEntity.ok(SuccessResponse.noContent()); } + @Operation(summary = "다른 유저 별명 설정", description = "자기 자신의 별명을 설정하려는 경우 에러 응답을 반환합니다. 별명을 제거하는 경우 null을 입력합니다. 별명은 공백을 허용하지 않습니다.") + @Parameter(name = "member_id", description = "별명을 설정할 유저 ID", in = ParameterIn.PATH, required = true) + @PutMapping("/{member_id}/nickname") + @PreAuthorize("isAuthenticated() and #memberId != principal.userId") + public ResponseEntity putNickname(@PathVariable("member_id") Long memberId, @RequestBody @Validated MemberNicknamePutReq req, @AuthenticationPrincipal CustomUserDetails user) { + memberAccountUseCase.updateSomeoneNickname(user.getUserId(), memberId, req.nickname()); + return ResponseEntity.ok(SuccessResponse.noContent()); + } + @Operation(summary = "관리 중인 반려동물 날짜별 스케줄 전체 조회") @GetMapping("/{user_id}/schedules") @PreAuthorize("isAuthenticated() and #userId == 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 856c8cf0..f81f9224 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 @@ -5,13 +5,18 @@ import kr.co.fitapet.common.execption.BaseErrorCode; import kr.co.fitapet.common.execption.GlobalErrorException; import kr.co.fitapet.domain.common.redis.sms.type.SmsPrefix; -import kr.co.fitapet.domain.domains.member.domain.Manager; +import kr.co.fitapet.domain.domains.manager.domain.Manager; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; import kr.co.fitapet.domain.domains.member.domain.Member; +import kr.co.fitapet.domain.domains.member.domain.MemberNickname; import kr.co.fitapet.domain.domains.member.dto.AccountProfileRes; import kr.co.fitapet.api.apis.profile.dto.AccountSearchReq; import kr.co.fitapet.api.apis.profile.dto.ProfilePatchReq; +import kr.co.fitapet.domain.domains.member.dto.MemberInfo; import kr.co.fitapet.domain.domains.member.dto.UidRes; import kr.co.fitapet.domain.domains.member.exception.AccountErrorCode; +import kr.co.fitapet.domain.domains.member.exception.AccountErrorException; +import kr.co.fitapet.domain.domains.member.service.MemberSaveService; import kr.co.fitapet.domain.domains.member.service.MemberSearchService; import kr.co.fitapet.domain.domains.member.type.MemberAttrType; import kr.co.fitapet.domain.domains.memo.dto.MemoCategoryInfoDto; @@ -37,6 +42,8 @@ public class MemberAccountUseCase { private final MemberSearchService memberSearchService; + private final ManagerSearchService managerSearchService; + private final ScheduleSearchService scheduleSearchService; private final MemoSearchService memoSearchService; @@ -50,6 +57,11 @@ public AccountProfileRes getProfile(Long userId) { return AccountProfileRes.from(member); } + @Transactional(readOnly = true) + public MemberInfo searchProfile(Long requesterId, String search) { + return memberSearchService.findMemberInfo(requesterId, search); + } + @Transactional(readOnly = true) public boolean existsUid(String uid) { return memberSearchService.isExistByUid(uid); @@ -70,9 +82,7 @@ public void updateProfile(Long userId, ProfilePatchReq req, MemberAttrType type) @Transactional(readOnly = true) public UidRes getUidWhenSmsAuthenticated(String phone, String code, SmsPrefix prefix) { - log.info("phone: {}, code: {}, prefix: {}", phone, code, prefix); validatePhone(phone, code, prefix); - log.info("isValid"); Member member = memberSearchService.findByPhone(phone); smsRedisMapper.removeCode(phone, prefix); return UidRes.of(member.getUid(), member.getCreatedAt()); @@ -98,9 +108,25 @@ public void updateNotification(Long requestId, Long userId, NotificationType typ member.updateNotificationFromType(type); } + @Transactional + public void updateSomeoneNickname(Long from, Long to, String nickname) { + Member fromMember = memberSearchService.findById(from); + Member toMember = memberSearchService.findById(to); + + MemberNickname memberNickname; + if (memberSearchService.isExistNicknameByFromAndTo(from, to)) { + log.info("기존 별명 업데이트 from : {}, to : {}, nickname : {}", from, to, nickname); + memberNickname = memberSearchService.findNicknameByFromAndTo(from, to); + memberNickname.updateNickname(nickname); + } else { + log.info("신규 별명 생성 from : {}, to : {}, nickname : {}", from, to, nickname); + memberNickname = MemberNickname.of(fromMember, toMember, nickname); + } + } + @Transactional(readOnly = true) public ScheduleInfoDto findPetSchedules(Long userId, LocalDateTime date) { - List pets = memberSearchService.findAllManagerByMemberId(userId) + List pets = managerSearchService.findAllByMemberId(userId) .stream().map(Manager::getPet).toList(); List petIds = pets.stream().map(Pet::getId).toList(); diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/schedule/usecase/ScheduleUseCase.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/schedule/usecase/ScheduleUseCase.java index a1724e9e..4521f3e4 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/schedule/usecase/ScheduleUseCase.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/apis/schedule/usecase/ScheduleUseCase.java @@ -2,6 +2,7 @@ import kr.co.fitapet.common.annotation.UseCase; import kr.co.fitapet.common.execption.GlobalErrorException; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; import kr.co.fitapet.domain.domains.member.service.MemberSearchService; import kr.co.fitapet.domain.domains.pet.domain.Pet; import kr.co.fitapet.domain.domains.pet.exception.PetErrorCode; @@ -24,7 +25,7 @@ @Slf4j @RequiredArgsConstructor public class ScheduleUseCase { - private final MemberSearchService memberSearchService; + private final ManagerSearchService managerSearchService; private final ScheduleSearchService scheduleSearchService; private final ScheduleSaveService scheduleSaveService; @@ -35,7 +36,7 @@ public class ScheduleUseCase { public void saveSchedule(Long userId, ScheduleSaveDto.Request request) { List petIds = request.petIds(); - if (!memberSearchService.isManagerAll(userId, petIds)) + if (!managerSearchService.isManagerAll(userId, petIds)) throw new GlobalErrorException(PetErrorCode.NOT_MANAGER_PET); Schedule schedule = scheduleSaveService.saveSchedule(request.toEntity()); diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/resolver/access/AccessTokenInfoResolver.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/resolver/access/AccessTokenInfoResolver.java index 78497534..1a54eaa2 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/resolver/access/AccessTokenInfoResolver.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/resolver/access/AccessTokenInfoResolver.java @@ -6,7 +6,7 @@ import kr.co.fitapet.api.common.security.jwt.dto.JwtSubInfo; import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorCode; import kr.co.fitapet.api.common.security.jwt.exception.AuthErrorException; -import kr.co.fitapet.domain.domains.member.domain.AccessToken; +import kr.co.fitapet.domain.common.redis.AccessToken; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/security/authorization/ManagerAuthorize.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/security/authorization/ManagerAuthorize.java index 314eaf22..960d911e 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/security/authorization/ManagerAuthorize.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/security/authorization/ManagerAuthorize.java @@ -1,6 +1,7 @@ package kr.co.fitapet.api.common.security.authorization; -import kr.co.fitapet.domain.domains.member.service.MemberSearchService; +import kr.co.fitapet.domain.common.redis.manager.ManagerInvitationService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; @@ -10,10 +11,22 @@ @RequiredArgsConstructor @Slf4j public class ManagerAuthorize { - private final MemberSearchService memberSearchService; + private final ManagerSearchService managerSearchService; + private final ManagerInvitationService managerInvitationService; @Cacheable(value = "manager", key = "#memberId + '@' + #petId", unless = "#result == false", cacheManager = "managerCacheManager") public boolean isManager(Long memberId, Long petId) { - return memberSearchService.isManager(memberId, petId); + return managerSearchService.isManager(memberId, petId); + } + + @Cacheable(value = "master", key = "#memberId + '@' + #petId", unless = "#result == false", cacheManager = "managerCacheManager") + public boolean isMaster(Long memberId, Long petId) { + Long masterId = managerSearchService.findMasterIdByPetId(petId); + log.info("masterId: {}", masterId); + return memberId.equals(masterId); + } + + public boolean isInvitedMember(Long memberId, Long petId) { + return !managerInvitationService.expired(memberId, petId); } } diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/security/authorization/ManagerPermissionExpression.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/security/authorization/ManagerPermissionExpression.java new file mode 100644 index 00000000..6a5b8eb8 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/common/security/authorization/ManagerPermissionExpression.java @@ -0,0 +1,25 @@ +package kr.co.fitapet.api.common.security.authorization; + +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.core.Authentication; + +import java.io.Serializable; + +public class ManagerPermissionExpression implements PermissionEvaluator { + private final ManagerSearchService managerSearchService; + + public ManagerPermissionExpression(ManagerSearchService managerSearchService) { + this.managerSearchService = managerSearchService; + } + + @Override + public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { + return false; + } + + @Override + public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { + return false; + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/config/security/CustomMethodSecurity.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/config/security/CustomMethodSecurity.java new file mode 100644 index 00000000..2751c4d3 --- /dev/null +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/config/security/CustomMethodSecurity.java @@ -0,0 +1,30 @@ +package kr.co.fitapet.api.config.security; + +import kr.co.fitapet.api.common.security.authorization.ManagerPermissionExpression; +import kr.co.fitapet.domain.domains.care.service.CareSearchService; +import kr.co.fitapet.domain.domains.manager.service.ManagerSearchService; +import kr.co.fitapet.domain.domains.memo.service.MemoSearchService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +@Configuration +@EnableMethodSecurity(securedEnabled = true) +@RequiredArgsConstructor +public class CustomMethodSecurity { + private final ManagerSearchService managerSearchService; + private final CareSearchService careSearchService; +// private final CareDateSearchService careDateSearchService; + private final MemoSearchService memoSearchService; +// private final MemoCategorySearchService memoCategorySearchService; + + @Bean + protected MethodSecurityExpressionHandler createExpressionHandler() { + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + expressionHandler.setPermissionEvaluator(new ManagerPermissionExpression(managerSearchService)); + return expressionHandler; + } +} diff --git a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/config/security/SecurityConfig.java b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/config/security/SecurityConfig.java index a921bb8e..22d3634c 100644 --- a/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/config/security/SecurityConfig.java +++ b/fitapet-app-external-api/src/main/java/kr/co/fitapet/api/config/security/SecurityConfig.java @@ -24,7 +24,6 @@ @Configuration @EnableWebSecurity -@EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtSecurityConfig jwtSecurityConfig; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/converter/ManageTypeConverter.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/converter/ManageTypeConverter.java index d14d84e1..ee9e410a 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/converter/ManageTypeConverter.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/converter/ManageTypeConverter.java @@ -2,7 +2,7 @@ import jakarta.persistence.Converter; import kr.co.fitapet.domain.common.util.converter.AbstractLegacyEnumAttributeConverter; -import kr.co.fitapet.domain.domains.member.type.ManageType; +import kr.co.fitapet.domain.domains.manager.type.ManageType; @Converter public class ManageTypeConverter extends AbstractLegacyEnumAttributeConverter { diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/AccessToken.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/AccessToken.java similarity index 85% rename from fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/AccessToken.java rename to fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/AccessToken.java index 81d97fab..bc3a4fe9 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/AccessToken.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/AccessToken.java @@ -1,4 +1,4 @@ -package kr.co.fitapet.domain.domains.member.domain; +package kr.co.fitapet.domain.common.redis; import java.time.LocalDateTime; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/exception/RedisErrorCode.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/exception/RedisErrorCode.java index 982728f8..939e523e 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/exception/RedisErrorCode.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/exception/RedisErrorCode.java @@ -11,6 +11,7 @@ public enum RedisErrorCode implements BaseErrorCode { /* 400 BAD REQUEST */ MISS_MATCHED_VALUES(400, "값이 일치하지 않습니다."), EXPIRED_VALUE(400, "값이 만료되었습니다."), + ALREADY_EXISTS_VALUE(400, "이미 존재하는 값입니다."), /* 404 NOT FOUND */ NOT_FOUND_KEY(404, "키를 찾을 수 없습니다."), diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/forbidden/ForbiddenTokenService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/forbidden/ForbiddenTokenService.java index f51bd242..a395880f 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/forbidden/ForbiddenTokenService.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/forbidden/ForbiddenTokenService.java @@ -1,6 +1,6 @@ package kr.co.fitapet.domain.common.redis.forbidden; -import kr.co.fitapet.domain.domains.member.domain.AccessToken; +import kr.co.fitapet.domain.common.redis.AccessToken; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/InvitationDto.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/InvitationDto.java new file mode 100644 index 00000000..bb89e049 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/InvitationDto.java @@ -0,0 +1,21 @@ +package kr.co.fitapet.domain.common.redis.manager; + +import java.time.LocalDateTime; +import java.util.Objects; + +public record InvitationDto( + Long petId, + Long inviteId, + LocalDateTime ttl, + Boolean expired +) { + public InvitationDto { + Objects.requireNonNull(petId, "petId must be provided"); + Objects.requireNonNull(inviteId, "inviteId must be provided"); + Objects.requireNonNull(ttl, "ttl must be provided"); + } + + public static InvitationDto of(Long petId, Long inviteId, LocalDateTime ttl) { + return new InvitationDto(petId, inviteId, ttl.minusDays(1), ttl.isBefore(LocalDateTime.now())); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepository.java new file mode 100644 index 00000000..14a124f5 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepository.java @@ -0,0 +1,13 @@ +package kr.co.fitapet.domain.common.redis.manager; + +import java.time.LocalDateTime; +import java.util.Map; + +public interface ManagerInvitationRepository { + void save(String petId, Long invitedId, LocalDateTime ttl); + + Boolean exists(String petId, Long invitedId); + LocalDateTime getTtl(String petId, Long invitedId); + Map findAll(String petId); + void delete(String petId, Long invitedId); +} 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 new file mode 100644 index 00000000..03e45c40 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationRepositoryImpl.java @@ -0,0 +1,45 @@ +package kr.co.fitapet.domain.common.redis.manager; + +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.Map; + +@Repository +public class ManagerInvitationRepositoryImpl implements ManagerInvitationRepository { + private final HashOperations ops; + private static final String KEY = "managerInvitation"; + + public ManagerInvitationRepositoryImpl(RedisTemplate redisTemplate) { + this.ops = redisTemplate.opsForHash(); + } + + @Override + public void save(String petId, Long invitedId, LocalDateTime ttl) { + ops.put(KEY + ":" + petId, invitedId, ttl); + } + + @Override + public Boolean exists(String petId, Long invitedId) { + return ops.hasKey(KEY + ":" + petId, invitedId); + } + + @Override + public LocalDateTime getTtl(String petId, Long invitedId) { + return ops.get(KEY + ":" + petId, invitedId); + } + + @Override + public Map findAll(String petId) { + return ops.entries(KEY + ":" + petId); + } + + @Override + 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/ManagerInvitationService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationService.java new file mode 100644 index 00000000..b9d99461 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationService.java @@ -0,0 +1,35 @@ +package kr.co.fitapet.domain.common.redis.manager; + +import java.util.AbstractMap; +import java.util.List; + +public interface ManagerInvitationService { + /** + * 매니저 초대 정보를 저장 + * @param invitedId : 초대 받는 사람 아이디 + * @param petId : 초대할 반려동물 고유번호 + */ + void save(Long invitedId, Long petId); + + /** + * 반려동물 관리자로 초대한 정보 전체 조회 + * @param petId : 초대할 반려동물 고유번호 + * @return List : 초대 정보 리스트 + */ + List findAll(Long petId); + + /** + * 초대 만료 여부 확인 + * @param invitedId : 초대 받는 사람 아이디 + * @param petId : 초대할 반려동물 고유번호 + * @return 존재하면 true, 아니면 false + */ + boolean expired(Long invitedId, Long petId); + + /** + * 초대 정보 삭제 + * @param invitedId : 초대 받는 사람 아이디 + * @param petId : 초대할 반려동물 고유번호 + */ + void delete(Long invitedId, Long petId); +} 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 new file mode 100644 index 00000000..ef766f5a --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/redis/manager/ManagerInvitationServiceImpl.java @@ -0,0 +1,62 @@ +package kr.co.fitapet.domain.common.redis.manager; + +import kr.co.fitapet.domain.common.redis.exception.RedisErrorCode; +import kr.co.fitapet.domain.common.redis.exception.RedisErrorException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ManagerInvitationServiceImpl implements ManagerInvitationService { + private final ManagerInvitationRepository managerInvitationRepository; + + @Override + public void save(Long invitedId, Long petId) { + if (managerInvitationRepository.exists(petId.toString(), invitedId)) { + log.warn("already invited. about User : {}", invitedId); + throw new RedisErrorException(RedisErrorCode.ALREADY_EXISTS_VALUE); + } + + LocalDateTime timeToLive = LocalDateTime.now().plusDays(1); + log.info("manager invitation ttl : {}", timeToLive); + + managerInvitationRepository.save(petId.toString(), invitedId, timeToLive); + log.info("manager invitation registered. about User : {}", invitedId); + } + + @Override + public List findAll(Long petId) { + Map all = managerInvitationRepository.findAll(petId.toString()); + return all.entrySet().stream() + .map(entry -> InvitationDto.of(petId, entry.getKey(), entry.getValue())) + .toList(); + } + + @Override + public boolean expired(Long invitedId, Long petId) { + isValid(invitedId, petId); + LocalDateTime ttl = managerInvitationRepository.getTtl(petId.toString(), invitedId); + log.info("manager invitation ttl : {}", ttl); + + return ttl.isBefore(LocalDateTime.now()); + } + + @Override + public void delete(Long invitedId, Long petId) { + isValid(invitedId, petId); + managerInvitationRepository.delete(petId.toString(), invitedId); + } + + private void isValid(Long invitedId, Long petId) { + if (!managerInvitationRepository.exists(petId.toString(), invitedId)) { + log.warn("not found invitation. about User : {}", invitedId); + throw new RedisErrorException(RedisErrorCode.NOT_FOUND_KEY); + } + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/DefaultSearchQueryDslRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/DefaultSearchQueryDslRepository.java index 46f50d09..0e9aeb5f 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/DefaultSearchQueryDslRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/DefaultSearchQueryDslRepository.java @@ -34,7 +34,7 @@ public interface DefaultSearchQueryDslRepository { * QueryHandler queryHandler = query -> query.leftJoin(entityChild).on(entity.id.eq(entityChild.entity.id)); * Sort sort = Sort.by(Sort.Direction.DESC, entity.id); * - * return searchRepository.findList(predicate, queryHandler, ); + * return searchRepository.findList(predicate, queryHandler, sort); * } * } * } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedJpaRepositoryImpl.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedJpaRepositoryImpl.java index 610d5971..d8066330 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedJpaRepositoryImpl.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedJpaRepositoryImpl.java @@ -1,6 +1,7 @@ package kr.co.fitapet.domain.common.repository; import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; import kr.co.fitapet.domain.common.exception.DomainErrorCode; import kr.co.fitapet.domain.common.exception.DomainErrorException; import lombok.extern.slf4j.Slf4j; @@ -36,6 +37,16 @@ public T findByIdOrElseThrow(ID id) { return result; } + @Override + public boolean existsById(ID id) { + Assert.notNull(id, ID_MUST_NOT_BE_NULL); + Class domainType = getDomainClass(); + TypedQuery query = em.createQuery("select e.id from " + domainType.getSimpleName() + " e where e.id = :id", Long.class); + query.setParameter("id", id); + query.setMaxResults(1); + return query.getResultList().size() > 0; + } + // TODO: 2021-11-30. 이름에 의존적인 메서드 제거하고, 상태 패턴을 적용하여 의존도 낮추기 private String getClassName() { Class domainType = getDomainClass(); diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedRepositoryFactory.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedRepositoryFactory.java index df7e4702..eb960bd2 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedRepositoryFactory.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/repository/ExtendedRepositoryFactory.java @@ -2,8 +2,10 @@ import jakarta.persistence.EntityManager; +import org.springframework.data.jpa.repository.support.CrudMethodMetadata; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; @@ -42,15 +44,17 @@ protected RepositoryComposition.RepositoryFragments getRepositoryFragments(Repos this.getEntityInformation(metadata.getDomainType()), this.em ); + fragments = fragments.append(RepositoryComposition.RepositoryFragments.just(implExtendedJpa)); + } + if (DefaultSearchQueryDslRepository.class.isAssignableFrom(metadata.getRepositoryInterface())) { var implQueryDsl = super.instantiateClass( DefaultSearchQueryDslRepositoryImpl.class, this.getEntityInformation(metadata.getDomainType()), this.em ); - fragments = fragments.append(RepositoryComposition.RepositoryFragments.just(implExtendedJpa)) - .append(RepositoryComposition.RepositoryFragments.just(implQueryDsl)); + fragments = fragments.append(RepositoryComposition.RepositoryFragments.just(implQueryDsl)); } return fragments; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/util/QueryDslUtil.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/util/QueryDslUtil.java index 74281b35..9d9a2251 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/util/QueryDslUtil.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/util/QueryDslUtil.java @@ -21,13 +21,18 @@ public class QueryDslUtil { case NULLS_LAST -> OrderSpecifier.NullHandling.NullsLast; }; + public static BooleanExpression matchAgainstOneElemNaturalMode(final StringPath c1, final String target) { + if (!StringUtils.hasText(target)) { return null; } + return Expressions.booleanTemplate( "function('one_column_natural', {0}, {1})", c1, target); + } + /** * match_against 함수를 사용하여 memo 테이블의 title, content 컬럼과 target을 비교한다. * @param c1 : memo.title * @param c2 : memo.content * @param target : 검색어 */ - public static BooleanExpression matchAgainst(final StringPath c1, final StringPath c2, final String target) { + public static BooleanExpression matchAgainstTwoElemBooleanMode(final StringPath c1, final StringPath c2, final String target) { if (!StringUtils.hasText(target)) { return null; } String template = "'" + target + "*'"; return Expressions.booleanTemplate( "function('match_against', {0}, {1}, {2})", c1, c2, template); diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/validator/NotWhiteSpace.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/validator/NotWhiteSpace.java new file mode 100644 index 00000000..a8c254f6 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/validator/NotWhiteSpace.java @@ -0,0 +1,23 @@ +package kr.co.fitapet.domain.common.validator; + +import jakarta.validation.Constraint; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 문자열 전체에 공백이 포함되어 있지 않은지 검증하는 애노테이션
+ * null은 유효하다고 판단한다. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = NotWhiteSpaceValidator.class) +public @interface NotWhiteSpace { + String message() default "whitespace is not allowed"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/validator/NotWhiteSpaceValidator.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/validator/NotWhiteSpaceValidator.java new file mode 100644 index 00000000..4ec2f17a --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/common/validator/NotWhiteSpaceValidator.java @@ -0,0 +1,18 @@ +package kr.co.fitapet.domain.common.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class NotWhiteSpaceValidator implements ConstraintValidator { + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + return !hasWhiteSpace(value); + } + + private boolean hasWhiteSpace(String value) { + return value.chars().anyMatch(Character::isWhitespace); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/MySqlFunctionContributor.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/MySqlFunctionContributor.java index 551029bf..98c15031 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/MySqlFunctionContributor.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/MySqlFunctionContributor.java @@ -29,6 +29,7 @@ */ public class MySqlFunctionContributor implements FunctionContributor { private static final String FUNCTION_NAME = "match_against"; + private static final String ONE_COLUMN_NATURAL_PATTERN = "match(?1) against(?2 in natural language mode)"; private static final String TWO_COLUMN_BOOLEAN_PATTERN = "match(?1, ?2) against(?3 in boolean mode)"; @Override @@ -37,6 +38,7 @@ public void contributeFunctions(final FunctionContributions functionContribution TypeConfiguration typeConfiguration = functionContributions.getTypeConfiguration(); registry.registerPattern( FUNCTION_NAME, TWO_COLUMN_BOOLEAN_PATTERN, typeConfiguration.getBasicTypeRegistry().resolve(StandardBasicTypes.BOOLEAN) ); + registry.registerPattern("one_column_natural", ONE_COLUMN_NATURAL_PATTERN, typeConfiguration.getBasicTypeRegistry().resolve(StandardBasicTypes.BOOLEAN)); registry.registerPattern( "left", "left(?1, ?2)", typeConfiguration.getBasicTypeRegistry().resolve(StandardBasicTypes.STRING) ); } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/RedisConfig.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/RedisConfig.java index bb7cc491..18debd7c 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/RedisConfig.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/config/RedisConfig.java @@ -1,6 +1,9 @@ package kr.co.fitapet.domain.config; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import kr.co.fitapet.domain.common.annotation.RedisCacheConnectionFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; @@ -40,12 +43,16 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean + @Primary public RedisTemplate redisTemplate() { RedisTemplate template = new RedisTemplate<>(); + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); template.setConnectionFactory(redisConnectionFactory()); + template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); return template; } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareCategoryRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareCategoryRepository.java index 0ab99fc0..7230ed6c 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareCategoryRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareCategoryRepository.java @@ -2,10 +2,11 @@ import kr.co.fitapet.domain.common.repository.ExtendedJpaRepository; +import kr.co.fitapet.domain.common.repository.ExtendedRepository; import kr.co.fitapet.domain.domains.care.domain.CareCategory; import java.util.List; -public interface CareCategoryRepository extends ExtendedJpaRepository { +public interface CareCategoryRepository extends ExtendedRepository { List findAllByPet_Id(Long petId); } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareDateRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareDateRepository.java index 650582ad..cf6c2c87 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareDateRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/care/repository/CareDateRepository.java @@ -1,12 +1,12 @@ package kr.co.fitapet.domain.domains.care.repository; -import kr.co.fitapet.domain.common.repository.ExtendedJpaRepository; +import kr.co.fitapet.domain.common.repository.ExtendedRepository; import kr.co.fitapet.domain.domains.care.domain.CareDate; import kr.co.fitapet.domain.domains.care.type.WeekType; import java.util.List; -public interface CareDateRepository extends ExtendedJpaRepository { +public interface CareDateRepository extends ExtendedRepository { List findAllByCare_IdAndWeek(Long careId, WeekType week); boolean existsByIdAndCare_Id(Long careDateId, Long careId); } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/Manager.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/domain/Manager.java similarity index 77% rename from fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/Manager.java rename to fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/domain/Manager.java index 377ebfe9..03cfc885 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/Manager.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/domain/Manager.java @@ -1,9 +1,10 @@ -package kr.co.fitapet.domain.domains.member.domain; +package kr.co.fitapet.domain.domains.manager.domain; -import kr.co.fitapet.domain.common.model.DateAuditable; import jakarta.persistence.*; -import kr.co.fitapet.domain.domains.member.type.ManageType; import kr.co.fitapet.domain.common.converter.ManageTypeConverter; +import kr.co.fitapet.domain.common.model.DateAuditable; +import kr.co.fitapet.domain.domains.manager.type.ManageType; +import kr.co.fitapet.domain.domains.member.domain.Member; import kr.co.fitapet.domain.domains.pet.domain.Pet; import lombok.AccessLevel; import lombok.Getter; @@ -75,4 +76,16 @@ public void updatePet(Pet pet) { pet.getManagers().add(this); } } + + public void delegateMaster(Manager manager) { + assert this.getManageType().equals(ManageType.MASTER); + assert manager.getManageType().equals(ManageType.MANAGER); + + this.updateManageType(ManageType.MANAGER); + manager.updateManageType(ManageType.MASTER); + } + + private void updateManageType(ManageType manageType) { + this.manageType = manageType; + } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/dto/ManagerInfoRes.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/dto/ManagerInfoRes.java new file mode 100644 index 00000000..0e571dbf --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/dto/ManagerInfoRes.java @@ -0,0 +1,30 @@ +package kr.co.fitapet.domain.domains.manager.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import kr.co.fitapet.domain.domains.member.domain.Member; +import lombok.Builder; + +import java.util.Objects; + +@Builder +@Schema(description = "매니저 정보 응답") +public record ManagerInfoRes( + @Schema(description = "매니저 ID", example = "1") + Long id, + @Schema(description = "매니저 UID", example = "fitapet") + String uid, + @Schema(description = "매니저 이름", example = "피타펫") + String name, + @Schema(description = "매니저 프로필 이미지 URL", example = "https://fitapet.co.kr/fitapet.png") + String profileImageUrl, + @Schema(description = "마스터 여부", example = "true") + Boolean isMaster +) { + public ManagerInfoRes(Long id, String uid, String name, String profileImageUrl, Boolean isMaster) { + this.id = Objects.requireNonNull(id); + this.uid = Objects.requireNonNull(uid); + this.name = Objects.requireNonNull(name); + this.profileImageUrl = Objects.toString(profileImageUrl, ""); + this.isMaster = Objects.requireNonNull(isMaster); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerQueryDslRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerQueryDslRepository.java new file mode 100644 index 00000000..ab9a7644 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerQueryDslRepository.java @@ -0,0 +1,10 @@ +package kr.co.fitapet.domain.domains.manager.repository; + +import kr.co.fitapet.domain.domains.manager.dto.ManagerInfoRes; + +import java.util.List; + +public interface ManagerQueryDslRepository { + Long findMasterIdByPetId(Long petId); + List findAllManager(Long petId, Long memberId); +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerQueryDslRepositoryImpl.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerQueryDslRepositoryImpl.java new file mode 100644 index 00000000..11c7e817 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerQueryDslRepositoryImpl.java @@ -0,0 +1,53 @@ +package kr.co.fitapet.domain.domains.manager.repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.QList; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import kr.co.fitapet.domain.domains.manager.domain.QManager; +import kr.co.fitapet.domain.domains.manager.dto.ManagerInfoRes; +import kr.co.fitapet.domain.domains.manager.type.ManageType; +import kr.co.fitapet.domain.domains.member.domain.Member; +import kr.co.fitapet.domain.domains.member.domain.QMember; +import kr.co.fitapet.domain.domains.member.domain.QMemberNickname; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ManagerQueryDslRepositoryImpl implements ManagerQueryDslRepository { + private final JPAQueryFactory queryFactory; + private final QMember member = QMember.member; + private final QManager manager = QManager.manager; + private final QMemberNickname nickname = QMemberNickname.memberNickname; + + @Override + public Long findMasterIdByPetId(Long petId) { + return queryFactory.select(manager.member.id) + .from(manager) + .where(manager.pet.id.eq(petId) + .and(manager.manageType.eq(ManageType.MASTER))) + .fetchFirst(); + } + + @Override + public List findAllManager(Long petId, Long memberId) { + return queryFactory + .select( + Projections.constructor( + ManagerInfoRes.class, + member.id, member.uid, + nickname.nickname.coalesce(member.name), + member.profileImg, + new CaseBuilder().when(manager.manageType.eq(ManageType.MASTER)).then(true).otherwise(false) + ) + ) + .from(manager) + .leftJoin(member).on(manager.member.id.eq(member.id)) + .leftJoin(nickname).on(member.id.eq(nickname.to.id).and(nickname.from.id.eq(memberId))) + .where(manager.pet.id.eq(petId)) + .fetch(); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerRepository.java new file mode 100644 index 00000000..88c597a2 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/repository/ManagerRepository.java @@ -0,0 +1,15 @@ +package kr.co.fitapet.domain.domains.manager.repository; + + +import kr.co.fitapet.domain.common.repository.ExtendedRepository; +import kr.co.fitapet.domain.domains.manager.domain.Manager; +import kr.co.fitapet.domain.domains.manager.type.ManageType; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface ManagerRepository extends ExtendedRepository, ManagerQueryDslRepository { + boolean existsByMember_IdAndPet_Id(Long memberId, Long petId); + List findAllByMember_Id(Long memberId); + Manager findByMember_IdAndPet_Id(Long memberId, Long petId); +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerDeleteService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerDeleteService.java new file mode 100644 index 00000000..1dcc311a --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerDeleteService.java @@ -0,0 +1,18 @@ +package kr.co.fitapet.domain.domains.manager.service; + +import kr.co.fitapet.common.annotation.DomainService; +import kr.co.fitapet.domain.domains.manager.domain.Manager; +import kr.co.fitapet.domain.domains.manager.repository.ManagerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@DomainService +@RequiredArgsConstructor +public class ManagerDeleteService { + private final ManagerRepository managerRepository; + + @Transactional + public void deleteManager(Manager manager) { + managerRepository.delete(manager); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerSaveService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerSaveService.java new file mode 100644 index 00000000..6e4e02f2 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerSaveService.java @@ -0,0 +1,25 @@ +package kr.co.fitapet.domain.domains.manager.service; + +import kr.co.fitapet.common.annotation.UseCase; +import kr.co.fitapet.domain.domains.manager.domain.Manager; +import kr.co.fitapet.domain.domains.manager.repository.ManagerRepository; +import kr.co.fitapet.domain.domains.member.domain.Member; +import kr.co.fitapet.domain.domains.manager.type.ManageType; +import kr.co.fitapet.domain.domains.pet.domain.Pet; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@UseCase +@RequiredArgsConstructor +public class ManagerSaveService { + private final ManagerRepository managerRepository; + + @Transactional + public void mappingMemberAndPet(Member member, Pet pet, ManageType manageType) { + Manager manager = Manager.of(member, pet, false, manageType); + managerRepository.save(manager); + } +} + diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerSearchService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerSearchService.java new file mode 100644 index 00000000..6d675b56 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/service/ManagerSearchService.java @@ -0,0 +1,57 @@ +package kr.co.fitapet.domain.domains.manager.service; + +import kr.co.fitapet.common.annotation.DomainService; +import kr.co.fitapet.domain.domains.manager.domain.Manager; +import kr.co.fitapet.domain.domains.manager.dto.ManagerInfoRes; +import kr.co.fitapet.domain.domains.manager.repository.ManagerRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@DomainService +@RequiredArgsConstructor +public class ManagerSearchService { + private final ManagerRepository managerRepository; + + @Transactional(readOnly = true) + public List findAllByMemberId(Long memberId) { + return managerRepository.findAllByMember_Id(memberId); + } + + @Transactional(readOnly = true) + public boolean isManager(Long memberId, Long petId) { + return managerRepository.existsByMember_IdAndPet_Id(memberId, petId); + } + + @Transactional(readOnly = true) + public boolean isManagerAll(Long memberId, List petIds) { + if (petIds.isEmpty()) { + return true; + } + + for (Long petId : petIds) { + if (!isManager(memberId, petId)) { + return false; + } + } + return true; + } + + @Transactional(readOnly = true) + public Long findMasterIdByPetId(Long petId) { + return managerRepository.findMasterIdByPetId(petId); + } + + @Transactional(readOnly = true) + public List findAllByPetId(Long petId, Long memberId) { + return managerRepository.findAllManager(petId, memberId); + } + + @Transactional(readOnly = true) + public Manager findByMemberIdAndPetId(Long memberId, Long petId) { + return managerRepository.findByMember_IdAndPet_Id(memberId, petId); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/type/ManageType.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/type/ManageType.java similarity index 93% rename from fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/type/ManageType.java rename to fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/type/ManageType.java index f28c3711..2904d796 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/type/ManageType.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/manager/type/ManageType.java @@ -1,4 +1,4 @@ -package kr.co.fitapet.domain.domains.member.type; +package kr.co.fitapet.domain.domains.manager.type; import com.fasterxml.jackson.annotation.JsonCreator; import kr.co.fitapet.domain.common.util.converter.LegacyCommonType; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/Member.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/Member.java index eb561ffa..8395efea 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/Member.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/Member.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import kr.co.fitapet.domain.common.model.DateAuditable; +import kr.co.fitapet.domain.domains.manager.domain.Manager; import kr.co.fitapet.domain.domains.member.type.RoleType; import kr.co.fitapet.domain.common.converter.RoleTypeConverter; import kr.co.fitapet.domain.domains.notification.domain.Notification; @@ -31,11 +32,11 @@ public class Member extends DateAuditable { private String password; private String phone; private String email; - @Column(name = "profile_img") @ColumnDefault("NULL") @Getter + @Column(name = "profile_img") @ColumnDefault("NULL") private String profileImg; @Column(name = "account_locked") @ColumnDefault("false") private Boolean accountLocked; - @Column(name = "is_oauth") @ColumnDefault("false") @Getter + @Column(name = "is_oauth") @ColumnDefault("false") private Boolean isOauth; @Convert(converter = RoleTypeConverter.class) private RoleType role; @@ -50,9 +51,9 @@ public class Member extends DateAuditable { private Set toMemberNickname = new HashSet<>(); @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List notifications = new ArrayList<>(); - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) @Getter + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List underCares = new ArrayList<>(); - @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) @Getter + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List oauthAccounts = new ArrayList<>(); @Builder diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/MemberNickname.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/MemberNickname.java index 63540b1a..73b3f1aa 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/MemberNickname.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/domain/MemberNickname.java @@ -2,14 +2,14 @@ import jakarta.persistence.*; import kr.co.fitapet.domain.common.model.DateAuditable; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.NoArgsConstructor; -import lombok.ToString; +import lombok.*; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "MEMBER_NICKNAME") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter @ToString(of = {"id", "nickname"}) public class MemberNickname extends DateAuditable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -17,21 +17,58 @@ public class MemberNickname extends DateAuditable { private String nickname; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "from") + @JoinColumn(name = "from_id") private Member from; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "to") + @JoinColumn(name = "to_id") private Member to; @Builder - private MemberNickname(String nickname) { + private MemberNickname(Member from, Member to, String nickname) { + this.updateFrom(from); + this.updateTo(to); this.nickname = nickname; } - public static MemberNickname of(String nickname) { + public static MemberNickname of(Member from, Member to, String nickname) { return MemberNickname.builder() + .from(from) + .to(to) .nickname(nickname) .build(); } + + public void updateAssociation(Member from, Member to) { + this.updateFrom(from); + this.updateTo(to); + } + + private void updateTo(Member to) { + if (this.to != null) { + this.to.getToMemberNickname().remove(this); + } + + this.to = to; + + if (to != null) { + to.getToMemberNickname().add(this); + } + } + + private void updateFrom(Member from) { + if (this.from != null) { + this.from.getFromMemberNickname().remove(this); + } + + this.from = from; + + if (from != null) { + from.getFromMemberNickname().add(this); + } + } + + public void updateNickname(String nickname) { + this.nickname = nickname; + } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/dto/MemberInfo.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/dto/MemberInfo.java new file mode 100644 index 00000000..132a9698 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/dto/MemberInfo.java @@ -0,0 +1,22 @@ +package kr.co.fitapet.domain.domains.member.dto; + +import kr.co.fitapet.common.annotation.Dto; +import lombok.Builder; + +import java.util.Objects; + +@Builder +@Dto(name = "member") +public record MemberInfo( + Long id, + String uid, + String name, + String profileImageUrl +) { + public MemberInfo(Long id, String uid, String name, String profileImageUrl) { + this.id = Objects.requireNonNull(id); + this.uid = Objects.requireNonNull(uid); + this.name = Objects.requireNonNull(name); + this.profileImageUrl = Objects.toString(profileImageUrl, ""); + } +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/dto/MemberNicknamePutReq.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/dto/MemberNicknamePutReq.java new file mode 100644 index 00000000..6995ed60 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/dto/MemberNicknamePutReq.java @@ -0,0 +1,14 @@ +package kr.co.fitapet.domain.domains.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import kr.co.fitapet.domain.common.validator.NotWhiteSpace; + +@Schema(description = "닉네임 수정 요청") +public record MemberNicknamePutReq( + @Schema(description = "닉네임", example = "닉네임", requiredMode = Schema.RequiredMode.REQUIRED) + @NotWhiteSpace + @Size(min = 2, max = 20) + String nickname +) { +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorCode.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorCode.java index 465bc778..ed955f79 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorCode.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorCode.java @@ -12,6 +12,7 @@ @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public enum AccountErrorCode implements BaseErrorCode { + /* 400 BAD REQUEST */ DUPLICATE_USER_INFO_ERROR(BAD_REQUEST.getCode(), "중복된 유저정보(닉네임/이메일/전화번호)가 존재합니다."), DUPLICATE_PHONE_ERROR(BAD_REQUEST.getCode(), "중복된 전화번호가 존재합니다."), @@ -23,12 +24,17 @@ public enum AccountErrorCode implements BaseErrorCode { NOT_CHANGE_NAME_ERROR(BAD_REQUEST.getCode(), "잘못된 닉네임 변경 요청입니다."), INVALID_NOTIFICATION_TYPE_ERROR(BAD_REQUEST.getCode(), "유효하지 않은 알림 타입입니다."), + INVALID_NICKNAME_ERROR(BAD_REQUEST.getCode(), "유효하지 않은 닉네임입니다."), MISSMATCH_PHONE_AND_UID_ERROR(BAD_REQUEST.getCode(), "등록된 전화번호와 일치하지 않는 유저입니다."), - /* 404 */ + ALREADY_MANAGER_ERROR(BAD_REQUEST.getCode(), "이미 관리자로 등록된 회원입니다."), + ALREADY_INVITED_ERROR(BAD_REQUEST.getCode(), "이미 초대된 회원입니다."), + + /* 404 NOT FOUND */ NOT_FOUND_MEMBER_ERROR(NOT_FOUND.getCode(), "존재하지 않는 회원입니다."), NOT_FOUND_PHONE_ERROR(NOT_FOUND.getCode(), "존재하지 않는 전화번호입니다."), + NOT_FOUND_NICKNAME_ERROR(NOT_FOUND.getCode(), "존재하지 않는 닉네임입니다."), ; private final int code; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorException.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorException.java index ff7b2e66..4da682d9 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorException.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/exception/AccountErrorException.java @@ -1,5 +1,6 @@ package kr.co.fitapet.domain.domains.member.exception; +import kr.co.fitapet.common.execption.BaseErrorCode; import kr.co.fitapet.common.execption.CausedBy; import kr.co.fitapet.common.execption.GlobalErrorException; @@ -14,4 +15,8 @@ public AccountErrorException(AccountErrorCode errorCode) { public CausedBy causedBy() { return errorCode.causedBy(); } + + public BaseErrorCode getErrorCode() { + return errorCode; + } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/ManagerRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/ManagerRepository.java deleted file mode 100644 index 82b62a20..00000000 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/ManagerRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package kr.co.fitapet.domain.domains.member.repository; - - -import kr.co.fitapet.domain.common.repository.ExtendedJpaRepository; -import kr.co.fitapet.domain.domains.member.domain.Manager; - -import java.util.List; - -public interface ManagerRepository extends ExtendedJpaRepository { - boolean existsByMember_IdAndPet_Id(Long memberId, Long petId); - List findAllByMember_Id(Long memberId); -} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberNicknameRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberNicknameRepository.java new file mode 100644 index 00000000..1710e7e0 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberNicknameRepository.java @@ -0,0 +1,11 @@ +package kr.co.fitapet.domain.domains.member.repository; + +import kr.co.fitapet.domain.common.repository.ExtendedRepository; +import kr.co.fitapet.domain.domains.member.domain.MemberNickname; + +import java.util.Optional; + +public interface MemberNicknameRepository extends ExtendedRepository { + boolean existsByFrom_IdAndTo_Id(Long fromId, Long toId); + Optional findByFrom_IdAndTo_Id(Long fromId, Long toId); +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepository.java index 268aaebc..92bf38f1 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepository.java @@ -1,7 +1,14 @@ package kr.co.fitapet.domain.domains.member.repository; +import kr.co.fitapet.domain.domains.member.domain.Member; +import kr.co.fitapet.domain.domains.member.dto.MemberInfo; + import java.util.List; +import java.util.Optional; public interface MemberQueryDslRepository { + List findByIds(List ids); List findMyPetIds(Long memberId); + Optional findMemberInfo(Long requesterId, String target); + List findMemberInfos(List memberIds, Long requesterId); } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepositoryImpl.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepositoryImpl.java index b00ea9d6..4866c5dc 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepositoryImpl.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberQueryDslRepositoryImpl.java @@ -1,22 +1,36 @@ package kr.co.fitapet.domain.domains.member.repository; +import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; -import kr.co.fitapet.domain.domains.member.domain.QManager; +import kr.co.fitapet.domain.common.util.QueryDslUtil; +import kr.co.fitapet.domain.domains.manager.domain.QManager; +import kr.co.fitapet.domain.domains.member.domain.Member; import kr.co.fitapet.domain.domains.member.domain.QMember; +import kr.co.fitapet.domain.domains.member.domain.QMemberNickname; +import kr.co.fitapet.domain.domains.member.dto.MemberInfo; import kr.co.fitapet.domain.domains.pet.domain.QPet; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository @RequiredArgsConstructor public class MemberQueryDslRepositoryImpl implements MemberQueryDslRepository { private final JPAQueryFactory queryFactory; private final QMember member = QMember.member; + private final QMemberNickname nickname = QMemberNickname.memberNickname; private final QManager manager = QManager.manager; private final QPet pet = QPet.pet; + @Override + public List findByIds(List ids) { + return queryFactory.selectFrom(member) + .where(member.id.in(ids)) + .fetch(); + } + @Override public List findMyPetIds(Long memberId) { return queryFactory.select(pet.id) @@ -26,4 +40,37 @@ public List findMyPetIds(Long memberId) { .where(member.id.eq(memberId)) .fetch(); } + + @Override + public Optional findMemberInfo(Long requesterId, String target) { + return Optional.ofNullable( + queryFactory + .select( + Projections.constructor( + MemberInfo.class, + member.id, member.uid, nickname.nickname.coalesce(member.name), member.profileImg + ) + ) + .from(member) + .leftJoin(nickname).on(member.id.eq(nickname.to.id).and(nickname.from.id.eq(requesterId))) + .where(QueryDslUtil.matchAgainstOneElemNaturalMode(member.uid, target)) + .fetchOne() + ); + } + + @Override + public List findMemberInfos(List memberIds, Long requesterId) { + return queryFactory + .select( + Projections.constructor( + MemberInfo.class, + member.id, member.uid, + nickname.nickname.coalesce(member.name), member.profileImg + ) + ) + .from(member) + .leftJoin(nickname).on(member.id.eq(nickname.to.id).and(nickname.from.id.eq(requesterId))) + .where(member.id.in(memberIds)) + .fetch(); + } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberRepository.java index 47e13ba0..1cdf4fe8 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/repository/MemberRepository.java @@ -1,6 +1,5 @@ package kr.co.fitapet.domain.domains.member.repository; - import kr.co.fitapet.domain.common.repository.ExtendedRepository; import kr.co.fitapet.domain.domains.member.domain.Member; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSaveService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSaveService.java index ee742606..eea03329 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSaveService.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSaveService.java @@ -1,30 +1,18 @@ package kr.co.fitapet.domain.domains.member.service; import kr.co.fitapet.common.annotation.DomainService; -import kr.co.fitapet.domain.domains.member.domain.Manager; import kr.co.fitapet.domain.domains.member.domain.Member; -import kr.co.fitapet.domain.domains.member.repository.ManagerRepository; import kr.co.fitapet.domain.domains.member.repository.MemberRepository; -import kr.co.fitapet.domain.domains.member.type.ManageType; -import kr.co.fitapet.domain.domains.pet.domain.Pet; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @DomainService @RequiredArgsConstructor public class MemberSaveService { private final MemberRepository memberRepository; - private final ManagerRepository managerRepository; @Transactional public Member saveMember(Member member) { return memberRepository.save(member); } - - @Transactional - public void mappingMemberAndPet(Member member, Pet pet, ManageType manageType) { - Manager manager = Manager.of(member, pet, false, manageType); - managerRepository.save(manager); - } } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSearchService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSearchService.java index 51bc8ea9..fd0cb62d 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSearchService.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/member/service/MemberSearchService.java @@ -2,14 +2,14 @@ import kr.co.fitapet.common.annotation.DomainService; -import kr.co.fitapet.domain.domains.member.domain.Manager; import kr.co.fitapet.domain.domains.member.domain.Member; +import kr.co.fitapet.domain.domains.member.domain.MemberNickname; +import kr.co.fitapet.domain.domains.member.dto.MemberInfo; import kr.co.fitapet.domain.domains.member.exception.AccountErrorCode; import kr.co.fitapet.domain.domains.member.exception.AccountErrorException; -import kr.co.fitapet.domain.domains.member.repository.ManagerRepository; +import kr.co.fitapet.domain.domains.member.repository.MemberNicknameRepository; import kr.co.fitapet.domain.domains.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -18,13 +18,25 @@ @RequiredArgsConstructor public class MemberSearchService { private final MemberRepository memberRepository; - private final ManagerRepository managerRepository; + private final MemberNicknameRepository memberNicknameRepository; @Transactional(readOnly = true) public Member findById(Long id) { return memberRepository.findByIdOrElseThrow(id); } + @Transactional(readOnly = true) + public MemberInfo findMemberInfo(Long requesterId, String target) { + return memberRepository.findMemberInfo(requesterId, target).orElseThrow( + () -> new AccountErrorException(AccountErrorCode.NOT_FOUND_MEMBER_ERROR) + ); + } + + @Transactional(readOnly = true) + public List findMemberInfos(List ids, Long requesterId) { + return memberRepository.findMemberInfos(ids, requesterId); + } + @Transactional(readOnly = true) public Member findByUid(String uid) { return memberRepository.findByUid(uid).orElseThrow( @@ -39,11 +51,6 @@ public Member findByPhone(String phone) { ); } - @Transactional(readOnly = true) - public List findAllManagerByMemberId(Long memberId) { - return managerRepository.findAllByMember_Id(memberId); - } - @Transactional(readOnly = true) public List findMyPetIds(Long memberId) { return memberRepository.findMyPetIds(memberId); @@ -54,6 +61,11 @@ public boolean isExistByUidOrPhone(String uid, String phone) { return memberRepository.existsByUidOrPhone(uid, phone); } + @Transactional(readOnly = true) + public boolean isExistById(Long id) { + return memberRepository.existsById(id); + } + @Transactional(readOnly = true) public boolean isExistByPhone(String phone) { return memberRepository.existsByPhone(phone); @@ -70,21 +82,15 @@ public boolean isExistByPhoneAndUid(String phone, String uid) { } @Transactional(readOnly = true) - public boolean isManager(Long memberId, Long petId) { - return managerRepository.existsByMember_IdAndPet_Id(memberId, petId); + public boolean isExistNicknameByFromAndTo(Long from, Long to) { + return memberNicknameRepository.existsByFrom_IdAndTo_Id(from, to); } @Transactional(readOnly = true) - public boolean isManagerAll(Long memberId, List petIds) { - if (petIds.isEmpty()) { - return true; - } - - for (Long petId : petIds) { - if (!isManager(memberId, petId)) { - return false; - } - } - return true; + public MemberNickname findNicknameByFromAndTo(Long from, Long to) { + return memberNicknameRepository.findByFrom_IdAndTo_Id(from, to).orElseThrow( + () -> new AccountErrorException(AccountErrorCode.NOT_FOUND_NICKNAME_ERROR) + ); } + } 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 3c371843..31d8167a 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 @@ -96,7 +96,7 @@ public Slice findMemosInMemoCategory(Long memoCateg .from(memoCategory) .leftJoin(memo).on(memo.memoCategory.id.eq(memoCategory.id)) .where(memoCategory.id.eq(memoCategoryId) - .and(QueryDslUtil.matchAgainst(memo.title, memo.content, target)) + .and(QueryDslUtil.matchAgainstTwoElemBooleanMode(memo.title, memo.content, target)) ) .orderBy(QueryDslUtil.getOrderSpecifier(pageable.getSort()).toArray(OrderSpecifier[]::new)) .offset(pageable.getOffset()) 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 bb52171d..dc837b92 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 @@ -3,7 +3,7 @@ import jakarta.persistence.*; import kr.co.fitapet.domain.common.model.DateAuditable; import kr.co.fitapet.domain.domains.care.domain.CareCategory; -import kr.co.fitapet.domain.domains.member.domain.Manager; +import kr.co.fitapet.domain.domains.manager.domain.Manager; import kr.co.fitapet.domain.domains.memo.domain.MemoCategory; import kr.co.fitapet.domain.domains.pet.type.GenderType; import kr.co.fitapet.domain.common.converter.GenderTypeConverter; diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetRepository.java index 43cfc260..cc2180c7 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetRepository.java @@ -1,9 +1,9 @@ package kr.co.fitapet.domain.domains.pet.repository; -import kr.co.fitapet.domain.common.repository.ExtendedJpaRepository; +import kr.co.fitapet.domain.common.repository.ExtendedRepository; import kr.co.fitapet.domain.domains.pet.domain.Pet; -public interface PetRepository extends ExtendedJpaRepository { +public interface PetRepository extends ExtendedRepository { boolean existsById(Long id); } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetScheduleRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetScheduleRepository.java index 12adc312..d13fa8b2 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetScheduleRepository.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/pet/repository/PetScheduleRepository.java @@ -1,8 +1,8 @@ package kr.co.fitapet.domain.domains.pet.repository; -import kr.co.fitapet.domain.common.repository.ExtendedJpaRepository; +import kr.co.fitapet.domain.common.repository.ExtendedRepository; import kr.co.fitapet.domain.domains.pet.domain.PetSchedule; -public interface PetScheduleRepository extends ExtendedJpaRepository { +public interface PetScheduleRepository extends ExtendedRepository { } diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/repository/ScheduleJpaRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/repository/ScheduleJpaRepository.java deleted file mode 100644 index 5191ff3f..00000000 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/repository/ScheduleJpaRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package kr.co.fitapet.domain.domains.schedule.repository; - - -import kr.co.fitapet.domain.common.repository.ExtendedJpaRepository; -import kr.co.fitapet.domain.domains.schedule.domain.Schedule; - -import java.util.List; - -public interface ScheduleJpaRepository extends ExtendedJpaRepository, ScheduleQueryRepository { - public List findAllById(Long id); -} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/repository/ScheduleRepository.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/repository/ScheduleRepository.java new file mode 100644 index 00000000..4213cff8 --- /dev/null +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/repository/ScheduleRepository.java @@ -0,0 +1,11 @@ +package kr.co.fitapet.domain.domains.schedule.repository; + + +import kr.co.fitapet.domain.common.repository.ExtendedRepository; +import kr.co.fitapet.domain.domains.schedule.domain.Schedule; + +import java.util.List; + +public interface ScheduleRepository extends ExtendedRepository, ScheduleQueryRepository { + List findAllById(Long id); +} diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSaveService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSaveService.java index 6e528b73..df180958 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSaveService.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSaveService.java @@ -2,16 +2,15 @@ import kr.co.fitapet.common.annotation.DomainService; import kr.co.fitapet.domain.domains.schedule.domain.Schedule; -import kr.co.fitapet.domain.domains.schedule.repository.ScheduleJpaRepository; +import kr.co.fitapet.domain.domains.schedule.repository.ScheduleRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; @Slf4j @DomainService @RequiredArgsConstructor public class ScheduleSaveService { - private final ScheduleJpaRepository scheduleRepository; + private final ScheduleRepository scheduleRepository; public Schedule saveSchedule(Schedule schedule) { return scheduleRepository.save(schedule); diff --git a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSearchService.java b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSearchService.java index a3f9a009..474678fc 100644 --- a/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSearchService.java +++ b/fitapet-domain/src/main/java/kr/co/fitapet/domain/domains/schedule/service/ScheduleSearchService.java @@ -2,10 +2,9 @@ import kr.co.fitapet.common.annotation.DomainService; import kr.co.fitapet.domain.domains.schedule.dto.ScheduleInfoDto; -import kr.co.fitapet.domain.domains.schedule.repository.ScheduleJpaRepository; +import kr.co.fitapet.domain.domains.schedule.repository.ScheduleRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; @@ -15,7 +14,7 @@ @Slf4j @RequiredArgsConstructor public class ScheduleSearchService { - private final ScheduleJpaRepository scheduleRepository; + private final ScheduleRepository scheduleRepository; /** * pet_id로 반려동물이 등록된 해당 날짜의 schedule_id 리스트 조회