diff --git a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java index 4f78006..e29d822 100644 --- a/src/main/java/com/assu/server/domain/chat/controller/ChatController.java +++ b/src/main/java/com/assu/server/domain/chat/controller/ChatController.java @@ -1,9 +1,8 @@ package com.assu.server.domain.chat.controller; -import com.assu.server.domain.chat.dto.ChatRequestDTO; -import com.assu.server.domain.chat.dto.ChatResponseDTO; -import com.assu.server.domain.chat.dto.ChatRoomUpdateDTO; +import com.assu.server.domain.chat.dto.*; import com.assu.server.domain.chat.repository.MessageRepository; +import com.assu.server.domain.chat.service.BlockService; import com.assu.server.domain.chat.service.ChatService; import com.assu.server.global.apiPayload.code.status.SuccessStatus; import com.assu.server.global.util.PresenceTracker; @@ -29,6 +28,7 @@ public class ChatController { private final SimpMessagingTemplate simpMessagingTemplate; private final PresenceTracker presenceTracker; private final MessageRepository messageRepository; + private final BlockService blockService; @Operation( summary = "채팅방을 생성하는 API", @@ -139,4 +139,64 @@ public BaseResponse leaveChattingR Long memberId = pd.getMember().getId(); return BaseResponse.onSuccess(SuccessStatus._OK, chatService.leaveChattingRoom(roomId, memberId)); } + + @Operation( + summary = "상대방을 차단하는 API" + + "상대방을 차단합니다. 메시지를 주고받을 수 없습니다.", + description = "# [v1.0 (2025-09-25)]() 상대방을 차단합니다.\n"+ + "- memberId: Request Body, Long\n" + ) + @PostMapping("/block") + public BaseResponse block( + @AuthenticationPrincipal PrincipalDetails pd, + @RequestBody BlockRequestDTO.BlockMemberRequestDTO request + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.blockMember(memberId, request.getOpponentId())); + } + + @Operation( + summary = "상대방을 차단했는지 확인하는 API" + + "상대방을 차단했는지 여부를 알려줍니다.", + description = "# [v1.0 (2025-09-25)]() 상대방을 차단했는지 검사합니다.\n"+ + "- memberId: Request Body, Long\n" + ) + @GetMapping("/check/block/{opponentId}") + public BaseResponse checkBlock( + @AuthenticationPrincipal PrincipalDetails pd, + @PathVariable Long opponentId + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.checkBlock(memberId, opponentId)); + } + + @Operation( + summary = "상대방을 차단 해제하는 API" + + "상대방을 차단해제합니다. 앞으로 다시 메시지를 주고받을 수 있습니다.", + description = "# [v1.0 (2025-09-25)]() 상대방을 차단 해제합니다.\n"+ + "- memberId: Request Body, Long\n" + ) + @DeleteMapping("/unblock") + public BaseResponse unblock( + @AuthenticationPrincipal PrincipalDetails pd, + @RequestParam Long opponentId + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.unblockMember(memberId, opponentId)); + } + + @Operation( + summary = "차단한 대상을 조회합니다." + + "본인이 차단한 대상을 모두 조회합니다.", + description = "# [v1.0 (2025-09-25)]() 차단한 대상을 조회합니다..\n"+ + "- memberId: Request Body, Long\n" + ) + @GetMapping("/blockList") + public BaseResponse> getBlockList( + @AuthenticationPrincipal PrincipalDetails pd + ) { + Long memberId = pd.getMember().getId(); + return BaseResponse.onSuccess(SuccessStatus._OK, blockService.getMyBlockList(memberId)); + } + } diff --git a/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java b/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java new file mode 100644 index 0000000..2ce2c00 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/converter/BlockConverter.java @@ -0,0 +1,50 @@ +package com.assu.server.domain.chat.converter; + +import com.assu.server.domain.chat.dto.BlockResponseDTO; +import com.assu.server.domain.chat.entity.Block; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; + +import java.util.List; +import java.util.stream.Collectors; + +public class BlockConverter { + public static BlockResponseDTO.BlockMemberDTO toBlockDTO(Long blockedId, String blockedName) { + return BlockResponseDTO.BlockMemberDTO.builder() + .memberId(blockedId) + .name(blockedName) + .build(); + } + + public static BlockResponseDTO.CheckBlockMemberDTO toCheckBlockDTO(Long blockedId, String blockedName, boolean blocked) { + return BlockResponseDTO.CheckBlockMemberDTO.builder() + .memberId(blockedId) + .name(blockedName) + .blocked(blocked) + .build(); + } + + public static BlockResponseDTO.BlockMemberDTO toBlockedMemberDTO(Block block) { + // Block 엔티티에서 차단된 사용자(Member) 정보를 꺼냅니다. + Member blockedMember = block.getBlocked(); + UserRole blockedRole = blockedMember.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blockedMember.getAdminProfile().getName(); + } else { + blockedName = blockedMember.getPartnerProfile().getName(); + } + + return BlockResponseDTO.BlockMemberDTO.builder() + .memberId(blockedMember.getId()) + .name(blockedName) // 또는 getNickname() 등 실제 필드명 사용 + .build(); + } + + public static List toBlockedMemberListDTO(List blockList) { + return blockList.stream() + .map(BlockConverter::toBlockedMemberDTO) // 각 Block 객체에 대해 위 헬퍼 메소드를 호출 + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java b/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java new file mode 100644 index 0000000..938b4f8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/BlockRequestDTO.java @@ -0,0 +1,13 @@ +package com.assu.server.domain.chat.dto; + +import lombok.Getter; +import lombok.Setter; + +public class BlockRequestDTO { + + @Getter + @Setter + public static class BlockMemberRequestDTO { + private Long opponentId; + } +} diff --git a/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java new file mode 100644 index 0000000..597ad69 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/dto/BlockResponseDTO.java @@ -0,0 +1,39 @@ +package com.assu.server.domain.chat.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +public class BlockResponseDTO { + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BlockMemberDTO { + private Long memberId; + private String name; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class CheckBlockMemberDTO { + private Long memberId; + private String name; + private boolean blocked; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class BlockedMemberListDTO { + List blockedMembers; + } + +} diff --git a/src/main/java/com/assu/server/domain/chat/entity/Block.java b/src/main/java/com/assu/server/domain/chat/entity/Block.java new file mode 100644 index 0000000..7aa99d1 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/entity/Block.java @@ -0,0 +1,31 @@ +package com.assu.server.domain.chat.entity; + +import com.assu.server.domain.common.entity.BaseEntity; +import com.assu.server.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Block extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "blocker_id", nullable = false) + private Member blocker; // 차단한 사람 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "blocked_id", nullable = false) + private Member blocked; // 차단당한 사람 + + @Builder + public Block(Member blocker, Member blocked) { + this.blocker = blocker; + this.blocked = blocked; + } +} diff --git a/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java new file mode 100644 index 0000000..3a69e2c --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/repository/BlockRepository.java @@ -0,0 +1,25 @@ +package com.assu.server.domain.chat.repository; + + +import com.assu.server.domain.chat.entity.Block; +import com.assu.server.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface BlockRepository extends JpaRepository { + boolean existsByBlockerAndBlocked(Member blocker, Member blocked); + + void deleteByBlockerAndBlocked(Member blocker, Member blocked); + + List findByBlocker(Member blocker); + + // BlockRepository.java + @Query("SELECT COUNT(b) > 0 FROM Block b " + + "WHERE (b.blocker = :user1 AND b.blocked = :user2) " + + "OR (b.blocker = :user2 AND b.blocked = :user1)") + boolean existsBlockRelationBetween(@Param("user1") Member user1, @Param("user2") Member user2); + +} diff --git a/src/main/java/com/assu/server/domain/chat/service/BlockService.java b/src/main/java/com/assu/server/domain/chat/service/BlockService.java new file mode 100644 index 0000000..27c0ac8 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/service/BlockService.java @@ -0,0 +1,12 @@ +package com.assu.server.domain.chat.service; + +import com.assu.server.domain.chat.dto.BlockResponseDTO; + +import java.util.List; + +public interface BlockService { + BlockResponseDTO.BlockMemberDTO blockMember(Long blockerId, Long blockedId); + BlockResponseDTO.CheckBlockMemberDTO checkBlock(Long blockerId, Long blockedId); + BlockResponseDTO.BlockMemberDTO unblockMember(Long blockerId, Long blockedId); + List getMyBlockList(Long blockerId); +} diff --git a/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java new file mode 100644 index 0000000..b9edc56 --- /dev/null +++ b/src/main/java/com/assu/server/domain/chat/service/BlockServiceImpl.java @@ -0,0 +1,124 @@ +package com.assu.server.domain.chat.service; + +import com.assu.server.domain.chat.converter.BlockConverter; +import com.assu.server.domain.chat.converter.ChatConverter; +import com.assu.server.domain.chat.dto.BlockResponseDTO; +import com.assu.server.domain.chat.entity.Block; +import com.assu.server.domain.chat.repository.BlockRepository; +import com.assu.server.domain.common.enums.UserRole; +import com.assu.server.domain.member.entity.Member; +import com.assu.server.domain.member.repository.MemberRepository; +import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.global.exception.GeneralException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BlockServiceImpl implements BlockService { + private final BlockRepository blockRepository; + private final MemberRepository memberRepository; + + @Transactional + @Override + public BlockResponseDTO.BlockMemberDTO blockMember(Long blockerId, Long blockedId) { + if (blockerId.equals(blockedId)) { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + Member blocked = memberRepository.findById(blockedId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + + + // 이미 차단했는지 확인 + if (blockRepository.existsByBlockerAndBlocked(blocker, blocked)) { + // 이미 차단한 경우, 아무것도 하지 않거나 예외 처리 + return null; + } + + UserRole blockedRole = blocked.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blocked.getAdminProfile().getName(); + } else if (blockedRole == UserRole.PARTNER) { + blockedName = blocked.getPartnerProfile().getName(); + } else { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + Block block = Block.builder() + .blocker(blocker) + .blocked(blocked) + .build(); + + blockRepository.save(block); + + return BlockConverter.toBlockDTO(blockedId, blockedName); + } + + @Override + public BlockResponseDTO.CheckBlockMemberDTO checkBlock(Long blockerId, Long blockedId) { + + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + Member blocked = memberRepository.findById(blockedId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + + UserRole blockedRole = blocked.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blocked.getAdminProfile().getName(); + } else if (blockedRole == UserRole.PARTNER) { + blockedName = blocked.getPartnerProfile().getName(); + } else { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + if (blockRepository.existsBlockRelationBetween(blocker, blocked)) { + return BlockConverter.toCheckBlockDTO(blockedId, blockedName, true); + } + else { + return BlockConverter.toCheckBlockDTO(blockedId, blockedName, false); + } + } + + @Transactional + @Override + public BlockResponseDTO.BlockMemberDTO unblockMember(Long blockerId, Long blockedId) { + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)); + Member blocked = memberRepository.findById(blockedId) + .orElseThrow(() -> new GeneralException(ErrorStatus.NO_SUCH_MEMBER)); + + UserRole blockedRole = blocked.getRole(); + String blockedName; + if (blockedRole == UserRole.ADMIN) { + blockedName = blocked.getAdminProfile().getName(); + } else if (blockedRole == UserRole.PARTNER) { + blockedName = blocked.getPartnerProfile().getName(); + } else { + throw new GeneralException(ErrorStatus._BAD_REQUEST); + } + + // Transactional 환경에서는 Dirty-checking으로 delete 쿼리가 나갑니다. + blockRepository.deleteByBlockerAndBlocked(blocker, blocked); + return BlockConverter.toBlockDTO(blockedId, blockedName); + } + + @Transactional + @Override + public List getMyBlockList(Long blockerId) { + Member blocker = memberRepository.findById(blockerId) + .orElseThrow(() -> new GeneralException(ErrorStatus._BAD_REQUEST)); + + List blockList = blockRepository.findByBlocker(blocker); + + return BlockConverter.toBlockedMemberListDTO(blockList); + } +} diff --git a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java index 62feb6d..084a963 100644 --- a/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/assu/server/domain/chat/service/ChatServiceImpl.java @@ -9,6 +9,7 @@ import com.assu.server.domain.chat.dto.ChatRoomListResultDTO; import com.assu.server.domain.chat.entity.ChattingRoom; import com.assu.server.domain.chat.entity.Message; +import com.assu.server.domain.chat.repository.BlockRepository; import com.assu.server.domain.chat.repository.ChatRepository; import com.assu.server.domain.chat.repository.MessageRepository; import com.assu.server.domain.member.entity.Member; @@ -38,6 +39,7 @@ public class ChatServiceImpl implements ChatService { private final AdminRepository adminRepository; private final MessageRepository messageRepository; private final StoreRepository storeRepository; + private final BlockRepository blockRepository; @Override @@ -95,8 +97,8 @@ public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatM log.info("saved message id={}, roomId={}, senderId={}, receiverId={}", saved.getId(), room.getId(), sender.getId(), receiver.getId()); - boolean exists = messageRepository.existsById(saved.getId()); - log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제 +// boolean exists = messageRepository.existsById(saved.getId()); +// log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제 return ChatConverter.toSendMessageDTO(saved); }