-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #113 from KCY-Fit-a-Pet/feat/93
✨ 관리자 API (Push Notification 기능 미적용 버전)
- Loading branch information
Showing
70 changed files
with
1,180 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
...t-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/controller/MangerApi.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = """ | ||
매니저가 반려동물을 탈퇴하거나 마스터가 추방하는 기능을 제공합니다. <br> | ||
매니저가 반려동물을 탈퇴하면 해당 반려동물의 매니저 목록에서 제거됩니다. <br> | ||
요청자와 매니저가 동일하지 않은 경우, 마스터 권한이 있는 경우에만 요청이 성공합니다. <br> | ||
요청자와 매니저가 동일하지만 요청자가 마스터인 경우 요청이 실패합니다. (마스터가 탈퇴하려면 반드시 다른 관리자에게 위임하거나, 반려동물 삭제 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()); | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
...pp-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberInfoRes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
...et-app-external-api/src/main/java/kr/co/fitapet/api/apis/manager/dto/InviteMemberReq.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package kr.co.fitapet.api.apis.manager.dto; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
|
||
public record InviteMemberReq( | ||
@NotNull | ||
Long inviteId | ||
) { | ||
} |
60 changes: 60 additions & 0 deletions
60
...rnal-api/src/main/java/kr/co/fitapet/api/apis/manager/mapper/ManagerInvitationMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<InviteMemberInfoRes> findInvitedMembers(Long petId, Long requesterId) { | ||
List<InvitationDto> invitationDtos = managerInvitationService.findAll(petId); | ||
List<MemberInfo> 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); | ||
} | ||
} |
Oops, something went wrong.