Togather : 추가 문제 해결과 채팅 코드 구현 (2)

2024. 12. 3. 06:12코딩 도구/프로젝트 개발 및 문제, 오류 해결

반응형

1편에 이어서 작성합니다. 

https://mkisos.tistory.com/entry/Togather-%EC%B1%84%ED%8C%85-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%EA%B3%BC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95-1

 

추가 수정 사항

  • 모집글 조회 시 모집 완료 여부(completed) 추가: 모집글 상세 페이지에서 모집이 완료된 경우 손들기/모집인원 UI를 모집 완료 UI로 표시하기 위해 completed 값을 추가했습니다.
  • 모집 완료 조건 추가: headCount == currentCount인 경우와 방장이 조기 마감을 실행한 경우 두 가지로 설정했습니다.

추가 문제 발견 및 해결

개발을 진행하면서 추가로 발견된 문제들을 해결했습니다.

문제 1: 채팅방 참가자 수 고정 문제 재발

현상: 이전에 수정했던 채팅방 참가자 수 문제가 다시 발생했습니다.

해결: 데이터베이스와 연동되는 부분에서 쿼리문에 오류가 있음을 발견했습니다. 쿼리문을 수정하여 모든 참가자가 정상적으로 채팅방에 참여할 수 있도록 했습니다.

문제 2: 조기 마감 후 손들기 수락 문제

현상: 조기 마감 후에도 방장이 알림에서 손들기 요청을 수락하면 새로운 채팅방이 생성되는 문제가 있었습니다.

해결: 모집이 완료된 후에는 손들기 수락이 불가능하도록 로직을 강화했습니다. 모집글의 상태를 확인하여 수락 불가 처리를 추가했습니다.

문제 3: 조기 마감 시 모집글 상태 업데이트

현상: 조기 마감 후에도 모집글 조회 시 completed 값이 true로 변경되지 않았습니다.

해결: 조기 마감 API에서 모집글의 completed 상태를 업데이트하도록 수정했습니다.

문제 4: lastMessage 초기화 문제

현상: 메시지를 읽었을 때 lastMessage가 0으로 초기화되어야 하는데, 메시지 전송 시에만 초기화되었습니다.

해결: 채팅방 상세 조회 시 lastMessage를 초기화하도록 로직을 수정했습니다.

문제 5: 손들기 거절 시 알림 제거

현상: 방장이 손들기 요청을 거절했을 때 알림 목록에서 해당 알림이 제거되지 않았습니다.

해결: 손들기 거절 API에서 알림 데이터를 삭제하도록 추가적인 처리를 했습니다.

채팅 관련 스프링 코드

아래는 채팅 기능 구현에 사용된 주요 코드입니다.

 

Chat 엔티티

@Entity
@Getter
@Setter
public class Chat {
    @Id
    @GeneratedValue
    @Column(name = "chat_id")
    private Long id;

    private String content;
    private LocalDateTime createdAt;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private Users user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "chatroom_id")
    private ChatRoom chatRoom;
}

 

ChatController

@Tag(name = "Chat", description = "채팅 관련 API 입니다.")
@RestController
@RequiredArgsConstructor
public class ChatController {

    private final ChatService chatService;

    // 손들기 요청
    @PostMapping("/raisehand/{groupBuyId}")
    public ResponseEntity<UserDto.CheckResult> raiseHand(
            @PathVariable Long groupBuyId, 
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        chatService.raiseHand(groupBuyId, userId);
        return ResponseEntity.ok(UserDto.CheckResult.builder().result("손들기 완료").build());
    }

    // 손들기 알림 목록 조회
    @GetMapping("/notifications")
    public ResponseEntity<List<WaitingNotificationDto>> getWaitingNotifications(
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        List<WaitingNotificationDto> notifications = chatService.getWaitingNotifications(userId);
        return ResponseEntity.ok(notifications);
    }

    // 손들기 수락
    @PutMapping("/acceptWaiting/{waitingId}")
    public ResponseEntity<UserDto.CheckResult> acceptWaiting(
            @PathVariable Long waitingId, 
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        chatService.acceptWaiting(waitingId, userId);
        return ResponseEntity.ok(UserDto.CheckResult.builder().result("수락 완료").build());
    }

    // 채팅방 목록 조회
    @GetMapping("/chatrooms")
    public ResponseEntity<List<ChatRoomListDto>> getChatRoomList(
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        List<ChatRoomListDto> chatRooms = chatService.getChatRoomList(userId);
        return ResponseEntity.ok(chatRooms);
    }

    // 채팅 메시지 전송
    @PutMapping("/chatrooms/{chatRoomId}/messages")
    public ResponseEntity<UserDto.CheckResult> sendMessage(
            @PathVariable Long chatRoomId, 
            @RequestBody ChatMessageDto chatMessageDto, 
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        chatService.sendMessage(chatRoomId, chatMessageDto, userId);
        return ResponseEntity.ok(UserDto.CheckResult.builder().result("메시지 전송 완료").build());
    }

    // 채팅방 상세 기록 조회
    @GetMapping("/chatrooms/{chatRoomId}/records")
    public ResponseEntity<List<ChatRecordDto>> getChatRecords(
            @PathVariable Long chatRoomId, 
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        List<ChatRecordDto> chatRecords = chatService.getChatRecords(chatRoomId, userId);
        return ResponseEntity.ok(chatRecords);
    }

    // 채팅방 나가기
    @DeleteMapping("/chatrooms/{chatRoomId}/leave")
    public ResponseEntity<UserDto.CheckResult> leaveChatRoom(
            @PathVariable Long chatRoomId, 
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        chatService.leaveChatRoom(chatRoomId, userId);
        return ResponseEntity.ok(UserDto.CheckResult.builder().result("채팅방 나가기 완료").build());
    }

    // 손들기 거절
    @DeleteMapping("/declineWaiting/{waitingId}")
    public ResponseEntity<UserDto.CheckResult> declineWaiting(
            @PathVariable Long waitingId, 
            @AuthenticationPrincipal UserDetails userDetails) {
        Long userId = Long.parseLong(userDetails.getUsername());
        chatService.declineWaiting(waitingId, userId);
        return ResponseEntity.ok(UserDto.CheckResult.builder().result("손들기 거절 완료").build());
    }
}

 

ChatService

@Service
@Transactional
@RequiredArgsConstructor
public class ChatService {

    private final WaitingRepository waitingRepository;
    private final GroupBuyRepository groupBuyRepository;
    private final UserRepository userRepository;
    private final ChatRoomRepository chatRoomRepository;
    private final UserChatRoomRepository userChatRoomRepository;
    private final ChatRepository chatRepository;

    // 손들기 요청 처리
    public void raiseHand(Long groupBuyId, Long userId) {
        Users user = userRepository.findById(userId)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        GroupBuy groupBuy = groupBuyRepository.findById(groupBuyId)
                .orElseThrow(() -> new IllegalArgumentException("해당 모집글이 존재하지 않습니다."));

        if (groupBuy.getUser().getId().equals(user.getId())) {
            throw new IllegalArgumentException("본인이 작성한 모집글에는 참여할 수 없습니다.");
        }

        // 중복 손들기 요청 방지
        Optional<Waiting> existingWaiting = waitingRepository.findByGroupBuyIdAndUserId(groupBuyId, userId);
        if (existingWaiting.isPresent()) {
            throw new IllegalStateException("이미 해당 모집글에 손들기 요청을 하였습니다.");
        }

        Waiting waiting = new Waiting();
        waiting.setUser(user);
        waiting.setGroupBuy(groupBuy);
        waitingRepository.save(waiting);
    }

    // 손들기 알림 목록 조회
    public List<WaitingNotificationDto> getWaitingNotifications(Long userId) {
        List<GroupBuy> groupBuys = groupBuyRepository.findByUserId(userId);

        return groupBuys.stream().flatMap(groupBuy -> {
            List<Waiting> waitings = waitingRepository.findByGroupBuyId(groupBuy.getId());

            // accept가 false인 대기자만 조회
            waitings = waitings.stream().filter(waiting -> !waiting.isAccepted()).collect(Collectors.toList());

            return waitings.stream().map(waiting -> new WaitingNotificationDto(
                    waiting.getId(),
                    waiting.getUser().getName(),
                    waiting.getUser().getPhoto(),
                    groupBuy.getTitle(),
                    groupBuy.getPhoto(),
                    waiting.getCreatedAt(),
                    groupBuy.getHeadCount(),
                    groupBuy.getCurrentCount()
            ));
        }).collect(Collectors.toList());
    }

    // 손들기 수락
    public void acceptWaiting(Long waitingId, Long userId) {
        Waiting waiting = waitingRepository.findById(waitingId)
                .orElseThrow(() -> new IllegalArgumentException("해당 대기자가 존재하지 않습니다."));
        GroupBuy groupBuy = waiting.getGroupBuy();

        // 모집 완료 상태 체크
        if (groupBuy.isCompleted()) {
            throw new IllegalStateException("모집이 완료되어 손들기 수락을 할 수 없습니다.");
        }

        waiting.setAccepted(true);
        waitingRepository.save(waiting);

        groupBuy.setCurrentCount(groupBuy.getCurrentCount() + 1);
        groupBuyRepository.save(groupBuy);

        // 채팅방 생성 조건 확인 및 생성
        if (groupBuy.getCurrentCount() >= groupBuy.getHeadCount()) {
            createChatRoomForGroupBuy(groupBuy);
        }
    }

    // 채팅방 생성
    public void createChatRoomForGroupBuy(GroupBuy groupBuy) {
        ChatRoom chatRoom = new ChatRoom();
        chatRoom.setGroupBuy(groupBuy);
        chatRoomRepository.save(chatRoom);

        // 모집된 인원을 채팅방에 추가
        List<Waiting> acceptedWaitings = waitingRepository.findByGroupBuyIdAndAccepted(groupBuy.getId(), true);
        for (Waiting acceptedWaiting : acceptedWaitings) {
            UserChatRoom userChatRoom = new UserChatRoom();
            userChatRoom.setUser(acceptedWaiting.getUser());
            userChatRoom.setChatRoom(chatRoom);
            userChatRoomRepository.save(userChatRoom);
        }

        // 파티장도 채팅방에 추가
        UserChatRoom ownerChatRoom = new UserChatRoom();
        ownerChatRoom.setUser(groupBuy.getUser());
        ownerChatRoom.setChatRoom(chatRoom);
        userChatRoomRepository.save(ownerChatRoom);
    }

    // 손들기 거절
    public void declineWaiting(Long waitingId, Long userId) {
        Waiting waiting = waitingRepository.findById(waitingId)
                .orElseThrow(() -> new IllegalArgumentException("해당 대기자가 존재하지 않습니다."));
        if (!waiting.getGroupBuy().getUser().getId().equals(userId)) {
            throw new IllegalArgumentException("손들기를 거절할 권한이 없습니다.");
        }
        waitingRepository.delete(waiting);
    }

    // 채팅방 목록 조회
    public List<ChatRoomListDto> getChatRoomList(Long userId) {
        List<UserChatRoom> userChatRooms = userChatRoomRepository.findByUserId(userId);

        return userChatRooms.stream().map(userChatRoom -> {
            ChatRoom chatRoom = userChatRoom.getChatRoom();
            List<String> participantImages = chatRoom.getUserChatRooms().stream()
                    .map(uc -> uc.getUser().getPhoto())
                    .collect(Collectors.toList());
            GroupBuy groupBuy = chatRoom.getGroupBuy();
            Chat lastChat = chatRepository.findTopByChatRoomOrderByCreatedAtDesc(chatRoom);

            return new ChatRoomListDto(
                    chatRoom.getId(),
                    participantImages,
                    groupBuy.getPhoto(),
                    groupBuy.getTitle(),
                    chatRoom.getUserChatRooms().size(),
                    lastChat != null ? lastChat.getContent() : "",
                    lastChat != null ? lastChat.getCreatedAt() : null,
                    userChatRoom.getUnreadMessageCount()
            );
        }).collect(Collectors.toList());
    }

    // 채팅 메시지 전송
    public void sendMessage(Long chatRoomId, ChatMessageDto chatMessageDto, Long userId) {
        Users user = userRepository.findById(userId)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
                .orElseThrow(() -> new IllegalArgumentException("해당 채팅방이 존재하지 않습니다."));

        Chat chat = new Chat();
        chat.setContent(chatMessageDto.getContent());
        chat.setCreatedAt(LocalDateTime.now());
        chat.setUser(user);
        chat.setChatRoom(chatRoom);
        chatRepository.save(chat);

        List<UserChatRoom> userChatRooms = userChatRoomRepository.findByChatRoomId(chatRoomId);
        for (UserChatRoom userChatRoom : userChatRooms) {
            if (!userChatRoom.getUser().getId().equals(userId)) {
                userChatRoom.setUnreadMessageCount(userChatRoom.getUnreadMessageCount() + 1);
            }
            userChatRoomRepository.save(userChatRoom);
        }
    }

    // 채팅방 상세 기록 조회
    public List<ChatRecordDto> getChatRecords(Long chatRoomId, Long userId) {
        Users currentUser = userRepository.findById(userId)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
                .orElseThrow(() -> new IllegalArgumentException("해당 채팅방이 존재하지 않습니다."));

        List<Chat> chats = chatRepository.findByChatRoomOrderByCreatedAtAsc(chatRoom);

        List<ChatRecordDto> chatRecords = new ArrayList<>();
        Long previousUserId = null;
        String chatRoomTitle = chatRoom.getGroupBuy().getTitle();

        for (Chat chat : chats) {
            boolean isContinuousMessage = previousUserId != null && previousUserId.equals(chat.getUser().getId());
            boolean isMyMessage = chat.getUser().getId().equals(currentUser.getId());

            chatRecords.add(new ChatRecordDto(
                    chat.getUser().getPhoto(),
                    chat.getUser().getName(),
                    chat.getContent(),
                    chat.getCreatedAt(),
                    isContinuousMessage,
                    isMyMessage,
                    chatRoomTitle
            ));

            previousUserId = chat.getUser().getId();
        }

        // 마지막 메시지 읽음 시간 및 미읽은 메시지 카운트 초기화
        Optional<UserChatRoom> optionalUserChatRoom = userChatRoomRepository.findByUserIdAndChatRoomId(userId, chatRoomId);
        UserChatRoom userChatRoom = optionalUserChatRoom.orElseThrow(() -> new IllegalArgumentException("해당 채팅방에 사용자가 존재하지 않습니다."));
        userChatRoom.setLastReadTime(LocalDateTime.now());
        userChatRoom.setUnreadMessageCount(0);
        userChatRoomRepository.save(userChatRoom);

        return chatRecords;
    }

    // 채팅방 나가기
    public void leaveChatRoom(Long chatRoomId, Long userId) {
        Optional<UserChatRoom> optionalUserChatRoom = userChatRoomRepository.findByUserIdAndChatRoomId(userId, chatRoomId);
        UserChatRoom userChatRoom = optionalUserChatRoom.orElseThrow(() -> new IllegalArgumentException("해당 채팅방에 사용자가 존재하지 않습니다."));

        userChatRoomRepository.delete(userChatRoom);
    }
}

 

마치며

이번 프로젝트를 통해 새로운 기술과 툴을 익히는 데 많은 도움을 받았습니다. 비록 처음 계획했던 WebSocket을 활용한 실시간 채팅 기능은 구현하지 못했지만, CRUD 방식을 통해 원하는 기능을 구현할 수 있었습니다. 앞으로도 지속적인 학습과 도전을 통해 더 나은 개발자가 되도록 노력하겠습니다.

 

긴 글 읽어주셔서 감사합니다. 이번 개발기를 통해 비슷한 문제를 겪고 있는 분들께 도움이 되었으면 좋겠습니다.

앞으로도 더욱 발전하는 모습을 보여드리겠습니다.

 

다른 자세한 코드는 아래 깃리포지토리에서 참고하세요.

https://github.com/cbnu-togather/togather-server

 

GitHub - cbnu-togather/togather-server: [Server Repo]

[Server Repo]. Contribute to cbnu-togather/togather-server development by creating an account on GitHub.

github.com

 

반응형