diff --git a/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomController.java b/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomController.java index e5e0ff02..0619ffe5 100644 --- a/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomController.java +++ b/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -82,4 +83,14 @@ public ResponseEntity cancelProgress( chatRoomService.cancelProgress(customOAuth2User.getMember(), chatRoomId); return ResponseEntity.ok().build(); } + + @Override + @PostMapping("/{chat-room-id}/leave") + @LogMonitoring + public ResponseEntity leaveChatRoom( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @PathVariable("chat-room-id") String chatRoomId) { + chatRoomService.leaveChatRoom(customOAuth2User.getMember(), chatRoomId); + return ResponseEntity.ok().build(); + } } diff --git a/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomControllerDocs.java b/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomControllerDocs.java index ff378c07..d91c8e01 100644 --- a/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomControllerDocs.java +++ b/ticketmate-api/src/main/java/com/ticketmate/backend/api/application/controller/chat/ChatRoomControllerDocs.java @@ -287,4 +287,42 @@ ResponseEntity chatRoomApplicationFormInfo( """ ) ResponseEntity cancelProgress(CustomOAuth2User customOAuth2User, String chatRoomId); + + @ApiChangeLogs({ + @ApiChangeLog( + date = "2026-01-28", + author = "mr6208", + description = "채팅방 나가기 API 설계", + issueUrl = "" + ) + }) + @Operation( + summary = "채팅방 나가기 기능", + description = """ + + 이 API는 인증이 필요합니다. + + ### 요청 파라미터 + - **chat-room-id (String)** : 채팅방 고유 ID [필수] + + ### 반환 데이터 + - 상태코드만을 반환합니다. + + ### 변경된 중요한 부분들 + - 카카오톡 오픈채팅을 레퍼런스로 참고해서 최대한 비슷하게 설계했습니다. + - 1:1 채팅도중 상대방이 나갔을 시 내 기준에서는 과거 채팅목록 조회가 가능하지만 채팅은 불가합니다. + - 상대방 기준에서는(나간사람) 채팅방 목록에 나간 채팅방이 보이지 않고 채팅방 내부 조회 자체가 불가능합니다. + - 이런 요구사항을 반영하기 위해 기존 응답데이터에 대한 변경이 이루어졌습니다. + + ### 아래는 채팅방 리스트업 및 채팅방 내부 조회시 추가된 데이터입니다. + ```json + { + "chatEnabled(현재 채팅이 가능한지 알려주는 플래그)": "true/false", + "opponentLeft(현재 채팅방에서 상대방이 나갔는지를 알려주는 플래그)": true/false + } + ``` + + """ + ) + ResponseEntity leaveChatRoom(CustomOAuth2User customOAuth2User, String chatRoomId); } diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomContextResponse.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomContextResponse.java index 47e5d128..24cc37af 100644 --- a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomContextResponse.java +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomContextResponse.java @@ -24,10 +24,13 @@ public class ChatRoomContextResponse { private List ticketOpenDateInfoResponseList; // 티켓 오픈일 List private TicketReservationSite ticketReservationSite; // 예매처 private ConcertType concertType; // 공연 카테고리 + private boolean chatEnabled; // 현재 채팅이 가능한지 + private boolean opponentLeft; // 상대방이 나갔는지 @Builder public ChatRoomContextResponse(String chatRoomId, UUID opponentMemberId, UUID fulfillmentFormId, String opponentMemberNickName, String concertName, String concertThumbnailUrl, - TicketOpenType ticketOpenType, List ticketOpenDateInfoResponseList, TicketReservationSite ticketReservationSite, ConcertType concertType) { + TicketOpenType ticketOpenType, List ticketOpenDateInfoResponseList, TicketReservationSite ticketReservationSite, ConcertType concertType, boolean chatEnabled, + boolean opponentLeft) { this.chatRoomId = chatRoomId; this.opponentMemberId = opponentMemberId; this.fulfillmentFormId = fulfillmentFormId; @@ -38,5 +41,7 @@ public ChatRoomContextResponse(String chatRoomId, UUID opponentMemberId, UUID fu this.ticketOpenDateInfoResponseList = ticketOpenDateInfoResponseList; this.ticketReservationSite = ticketReservationSite; this.concertType = concertType; + this.chatEnabled = chatEnabled; + this.opponentLeft = opponentLeft; } } diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomResponse.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomResponse.java index 6b41679c..914ef72c 100644 --- a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomResponse.java +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/dto/response/ChatRoomResponse.java @@ -11,6 +11,7 @@ @Setter @NoArgsConstructor public class ChatRoomResponse { + private String chatRoomId; // 채팅방 PK private String chatRoomName; // 상대방 닉네임 출력 private String lastChatMessage; // 마지막 채팅 메시지 @@ -19,10 +20,12 @@ public class ChatRoomResponse { private String concertThumbnailUrl; // 콘서트 썸네일 사진 private TicketOpenType ticketOpenType; // 선예매/일예 구분 private int unReadMessageCount; // 읽지 않은 메시지 개수 + private boolean chatEnabled; // 메시지 전송 가능 여부 + private boolean opponentLeft; // 상대가 나갔는지(전송불가) @Builder - public ChatRoomResponse(String chatRoomId, String chatRoomName, String lastChatMessage, LocalDateTime lastChatSendTime, String profileUrl, String concertThumbnailUrl, - TicketOpenType ticketOpenType, int unReadMessageCount) { + public ChatRoomResponse(String chatRoomId, String chatRoomName, String lastChatMessage, LocalDateTime lastChatSendTime, String profileUrl, String concertThumbnailUrl, TicketOpenType ticketOpenType, + int unReadMessageCount, boolean chatEnabled, boolean opponentLeft) { this.chatRoomId = chatRoomId; this.chatRoomName = chatRoomName; this.lastChatMessage = lastChatMessage; @@ -31,5 +34,7 @@ public ChatRoomResponse(String chatRoomId, String chatRoomName, String lastChatM this.concertThumbnailUrl = concertThumbnailUrl; this.ticketOpenType = ticketOpenType; this.unReadMessageCount = unReadMessageCount; + this.chatEnabled = chatEnabled; + this.opponentLeft = opponentLeft; } } diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/mapper/ChatMapperImpl.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/mapper/ChatMapperImpl.java index 04b45581..31864bc6 100644 --- a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/mapper/ChatMapperImpl.java +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/mapper/ChatMapperImpl.java @@ -56,6 +56,9 @@ public ChatRoomResponse toChatRoomResponse(ChatRoom chatRoom, Member member, Map String concertThumbnailStoredPath = applicationForm.getConcert().getConcertThumbnailStoredPath(); String profileImgStoredPath = other.getProfileImgStoredPath(); + boolean chatEnabled = chatRoom.canChat(); + boolean opponentLeft = chatRoom.isLeft(opponentId); + return ChatRoomResponse.builder() .unReadMessageCount(unRead) .chatRoomId(chatRoom.getChatRoomId()) @@ -65,12 +68,19 @@ public ChatRoomResponse toChatRoomResponse(ChatRoom chatRoom, Member member, Map .concertThumbnailUrl(storageService.generatePublicUrl(concertThumbnailStoredPath)) .lastChatSendTime(TimeUtil.toLocalDateTime(chatRoom.getLastMessageTime())) .profileUrl(storageService.generatePublicUrl(profileImgStoredPath)) + .chatEnabled(chatEnabled) + .opponentLeft(opponentLeft) .build(); } @Override public ChatRoomContextResponse toChatRoomContextResponse(ChatRoom chatRoom, UUID currentMemberId, UUID fulfillmentFormId, TicketOpenType ticketOpenType, String opponentMemberNickname, ConcertInfoResponse response) { + + UUID opponentId = chatRoom.getOpponentId(currentMemberId); + boolean chatEnabled = chatRoom.canChat(); + boolean opponentLeft = chatRoom.isLeft(opponentId); + return ChatRoomContextResponse.builder() .concertName(response.concertName()) .concertType(response.concertType()) @@ -82,6 +92,8 @@ public ChatRoomContextResponse toChatRoomContextResponse(ChatRoom chatRoom, UUID .ticketOpenDateInfoResponseList(response.ticketOpenDateInfoResponseList()) .chatRoomId(chatRoom.getChatRoomId()) .ticketOpenType(ticketOpenType) + .chatEnabled(chatEnabled) + .opponentLeft(opponentLeft) .build(); } } diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatMessageService.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatMessageService.java index 8a2cb548..06f44486 100644 --- a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatMessageService.java +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatMessageService.java @@ -266,6 +266,8 @@ private String formattingSendDate(LocalDateTime dateTime) { * 채팅 메시지 저장 + Redis를 이용해 읽지않은 메시지 count, 및 브로드캐스팅을 관리하는 메서드 */ private ChatMessage handleNewChatMessage(Member sender, ChatMessageRequest request, ChatRoom chatRoom) { + // 현재 채팅을 진행할 수 있는지 검증 + validateCanChat(chatRoom); ChatMessage message = saveChatMessage(sender, request, chatRoom); @@ -466,4 +468,11 @@ private String toPreview(ChatMessage chatMessage) { case UPDATE_FULFILLMENT_FORM -> ChatMessageType.UPDATE_FULFILLMENT_FORM.getDescription(); }; } + + // 현재 채팅을 진행 수 있는 상태인지(한명이라도 나가면 채팅 불가) + private void validateCanChat(ChatRoom room) { + if (!room.canChat()) { + throw new CustomException(ErrorCode.CHAT_DISABLED); + } + } } \ No newline at end of file diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatRoomService.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatRoomService.java index bb1bf5b1..a55d661c 100644 --- a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatRoomService.java +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/application/service/ChatRoomService.java @@ -22,6 +22,7 @@ import com.ticketmate.backend.concert.infrastructure.entity.Concert; import com.ticketmate.backend.member.infrastructure.entity.Member; import com.ticketmate.backend.member.infrastructure.repository.MemberRepository; +import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; @@ -169,8 +170,8 @@ public Slice getChatMessage(Member member, String chatRoomI ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId) .orElseThrow(() -> new CustomException(ErrorCode.CHAT_ROOM_NOT_FOUND)); - // 채팅방 내부 참가자 유효성 검증 - validateRoomMember(chatRoom, member); + // 채팅방 내부 참가자 유효성 검증 및 채팅방 유효성 검증 + validateActiveParticipant(chatRoom, member); // 채팅메시지 전용 페이지네이션 객체 생성 Pageable pageable = request.toPageable(); @@ -224,6 +225,26 @@ public void cancelProgress(Member member, String chatRoomId) { applicationForm.setApplicationFormStatus(ApplicationFormStatus.CANCELED_IN_PROCESS); } + // 채팅방 나가기 기능 + @Transactional + public void leaveChatRoom(Member member, String chatRoomId) { + ChatRoom chatRoom = findChatRoomById(chatRoomId); + validateRoomMember(chatRoom, member); + + UUID memberId = member.getMemberId(); + if (chatRoom.isLeft(memberId)) { + return; + } + + chatRoom.leave(memberId, Instant.now()); + chatRoomRepository.save(chatRoom); + + // unread 키 삭제 + String key = (UN_READ_MESSAGE_COUNTER_KEY).formatted(chatRoomId, memberId); + redisTemplate.delete(key); + log.debug("채팅방 나가기 완료. 채팅방 상태 : {}, 나간 시간: {}", chatRoom.getRoomStatus(), chatRoom.getClosedDate()); + } + /** * 방 참가자 검증 */ @@ -235,6 +256,14 @@ public void validateRoomMember(ChatRoom room, Member member) { } } + private void validateActiveParticipant(ChatRoom room, Member member) { + validateRoomMember(room, member); + + if (room.isLeft(member.getMemberId())) { + throw new CustomException(ErrorCode.CHAT_ROOM_LEFT); + } + } + /** * 채팅방 조회 공용 메서드 */ diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/core/constant/ChatRoomStatus.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/core/constant/ChatRoomStatus.java new file mode 100644 index 00000000..acd914e7 --- /dev/null +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/core/constant/ChatRoomStatus.java @@ -0,0 +1,15 @@ +package com.ticketmate.backend.chat.core.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ChatRoomStatus { + ACTIVE("열린 채팅방"), + + CLOSED("닫힌 채팅방"); + + private final String description; + +} diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/entity/ChatRoom.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/entity/ChatRoom.java index 58e1a413..62d0cc03 100644 --- a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/entity/ChatRoom.java +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/entity/ChatRoom.java @@ -1,12 +1,16 @@ package com.ticketmate.backend.chat.infrastructure.entity; import com.ticketmate.backend.chat.core.constant.ChatMessageType; +import com.ticketmate.backend.chat.core.constant.ChatRoomStatus; +import com.ticketmate.backend.common.application.exception.CustomException; +import com.ticketmate.backend.common.application.exception.ErrorCode; import com.ticketmate.backend.common.infrastructure.persistence.BaseMongoDocument; import com.ticketmate.backend.concert.core.constant.TicketOpenType; import java.time.Instant; import java.util.UUID; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -68,6 +72,19 @@ public class ChatRoom extends BaseMongoDocument { private TicketOpenType ticketOpenType; // 신청폼의 선예매, 일반예매인지 + @Indexed + private Instant agentLeftDate; // 대리인 퇴장시간 + + @Indexed + private Instant clientLeftDate; // 의뢰인 퇴장시간 + + @Indexed + @Builder.Default + private ChatRoomStatus roomStatus = ChatRoomStatus.ACTIVE; // 한명이라도 나가면 CLOSED 상태 + + @Indexed + private Instant closedDate; + public void updateLastMessage(String message) { this.lastMessage = message; } @@ -88,4 +105,40 @@ public void updateLastMessageType(ChatMessageType chatMessageType) { public UUID getOpponentId(UUID currentMemberId) { return currentMemberId.equals(agentMemberId) ? clientMemberId : agentMemberId; } + + public boolean isLeft(UUID memberId) { + if (memberId.equals(agentMemberId)) { + return agentLeftDate != null; + } + if (memberId.equals(clientMemberId)) { + return clientLeftDate != null; + } + return false; + } + + public boolean isParticipant(UUID memberId) { + return memberId.equals(agentMemberId) || memberId.equals(clientMemberId); + } + + public boolean canChat() { + // 상대가 나갔을시 남아있는 사람도 채팅 불가 + return roomStatus == ChatRoomStatus.ACTIVE && agentLeftDate == null && clientLeftDate == null; + } + + // 채팅방을 나갈 시 동작하는 메서드 + public void leave(UUID memberId, Instant now) { + if (memberId.equals(agentMemberId)) { + agentLeftDate = now; + } else if (memberId.equals(clientMemberId)) { + clientLeftDate = now; + } else { + throw new CustomException(ErrorCode.NO_AUTH_TO_ROOM); + } + + // 한 명이라도 나가면 방은 CLOSED + if (roomStatus != ChatRoomStatus.CLOSED) { + roomStatus = ChatRoomStatus.CLOSED; + closedDate = now; + } + } } diff --git a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/repository/ChatRoomRepositoryImpl.java b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/repository/ChatRoomRepositoryImpl.java index fee76728..f63673e7 100644 --- a/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/repository/ChatRoomRepositoryImpl.java +++ b/ticketmate-chat/src/main/java/com/ticketmate/backend/chat/infrastructure/repository/ChatRoomRepositoryImpl.java @@ -34,8 +34,9 @@ public Page search(TicketOpenType ticketOpenType, String keyword, Memb log.debug("선예매 일반예매 구분 : {}", ticketOpenType); - // 1. 내가 ‘대리인’으로 들어가 있는 방 조건 - Criteria asAgent = Criteria.where("agentMemberId").is(memberId); + // 1. 내가 ‘대리인’으로 들어가 있는 방 조건 + 나가지 않은 채팅방(나간시점의 기록이 없는) + Criteria asAgent = Criteria.where("agentMemberId").is(memberId).and("agentLeftAt").is(null); + if (ticketOpenType != null) { asAgent = asAgent.and("ticketOpenType").is(ticketOpenType); } @@ -43,8 +44,8 @@ public Page search(TicketOpenType ticketOpenType, String keyword, Memb asAgent = asAgent.and("clientMemberNickname").regex(keyword, "i"); } - // 2. 내가 ‘의뢰인’으로 들어가 있는 방 조건 - Criteria asClient = Criteria.where("clientMemberId").is(memberId); + // 2. 내가 ‘의뢰인’으로 들어가 있는 방 조건 + 위와 동일 + Criteria asClient = Criteria.where("clientMemberId").is(memberId).and("clientLeftAt").is(null); if (ticketOpenType != null) { asClient = asClient.and("ticketOpenType").is(ticketOpenType); } @@ -57,9 +58,9 @@ public Page search(TicketOpenType ticketOpenType, String keyword, Memb // 마지막 채팅 메시지 시간 기준 내림차순 정렬 (6개씩) Query query = Query.query(criteria) - .with(Sort.by(Sort.Direction.DESC, "lastMessageTime")) - .skip((long) pageNumber * PageableConstants.DEFAULT_PAGE_SIZE) - .limit(PageableConstants.DEFAULT_PAGE_SIZE); + .with(Sort.by(Sort.Direction.DESC, "lastMessageTime")) + .skip((long) pageNumber * PageableConstants.DEFAULT_PAGE_SIZE) + .limit(PageableConstants.DEFAULT_PAGE_SIZE); List chatRoomList = mongoTemplate.find(query, ChatRoom.class); log.debug("조회된 채팅방 객체 개수 : {}", chatRoomList.size()); @@ -68,7 +69,7 @@ public Page search(TicketOpenType ticketOpenType, String keyword, Memb log.debug("Count로 조회된 채팅방 개수 : {}", totalCount); return new PageImpl<>(chatRoomList, - PageRequest.of(pageNumber, PageableConstants.DEFAULT_PAGE_SIZE), - totalCount); + PageRequest.of(pageNumber, PageableConstants.DEFAULT_PAGE_SIZE), + totalCount); } } diff --git a/ticketmate-common/src/main/java/com/ticketmate/backend/common/application/exception/ErrorCode.java b/ticketmate-common/src/main/java/com/ticketmate/backend/common/application/exception/ErrorCode.java index 79198a2f..dc8aed87 100644 --- a/ticketmate-common/src/main/java/com/ticketmate/backend/common/application/exception/ErrorCode.java +++ b/ticketmate-common/src/main/java/com/ticketmate/backend/common/application/exception/ErrorCode.java @@ -399,6 +399,10 @@ public enum ErrorCode { CHAT_MESSAGE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "채팅 메시지 전송중 오류가 발생했습니다."), + CHAT_ROOM_LEFT(HttpStatus.NOT_FOUND, "채팅방을 나가서 존재하지 않는 채팅방입니다."), + + CHAT_DISABLED(HttpStatus.BAD_REQUEST, "해당 채팅방은 상대방이 채팅방을 나가 채팅을 진행할 수 없습니다."), + // EMBEDDING EMBEDDING_API_ERROR(HttpStatus.BAD_REQUEST, "Vertex AI API 호출에 실패했습니다."), diff --git a/ticketmate-fulfillmentform/src/main/java/com/ticketmate/backend/fulfillmentform/application/service/successhistory/SuccessHistoryService.java b/ticketmate-fulfillmentform/src/main/java/com/ticketmate/backend/fulfillmentform/application/service/successhistory/SuccessHistoryService.java index 05389453..3719ea6a 100644 --- a/ticketmate-fulfillmentform/src/main/java/com/ticketmate/backend/fulfillmentform/application/service/successhistory/SuccessHistoryService.java +++ b/ticketmate-fulfillmentform/src/main/java/com/ticketmate/backend/fulfillmentform/application/service/successhistory/SuccessHistoryService.java @@ -6,9 +6,6 @@ import com.ticketmate.backend.fulfillmentform.infrastructure.entity.FulfillmentForm; import com.ticketmate.backend.fulfillmentform.infrastructure.entity.SuccessHistory; import com.ticketmate.backend.fulfillmentform.infrastructure.repository.successhistory.SuccessHistoryRepository; -import com.ticketmate.backend.member.application.service.MemberService; -import com.ticketmate.backend.member.core.constant.MemberType; -import com.ticketmate.backend.member.infrastructure.entity.Member; import jakarta.persistence.EntityManager; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -25,7 +22,6 @@ public class SuccessHistoryService { private final SuccessHistoryRepository successHistoryRepository; private final EntityManager entityManager; - private final MemberService memberService; private final SuccessHistoryMapper mapper; public void createSuccessHistory(UUID fulfillmentFormId) { @@ -40,11 +36,6 @@ public void createSuccessHistory(UUID fulfillmentFormId) { @Transactional(readOnly = true) public Slice getSuccessHistoryList(UUID agentId, SuccessHistoryFilteredRequest request) { - Member agent = memberService.findMemberById(agentId); - - // 조회객체가 대리인이 정말 맞는지 - memberService.validateMemberType(agent, MemberType.AGENT); - // 페이지네이션 객체 생성 Pageable pageable = request.toPageable();