2024. 12. 3. 06:12ㆍ코딩 도구/프로젝트 개발 및 문제, 오류 해결
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
'코딩 도구 > 프로젝트 개발 및 문제, 오류 해결' 카테고리의 다른 글
LottoVerse : 미국의 Powerball 및 Mega Millions (2) | 2024.12.31 |
---|---|
CoLabor : 리뷰 등록 API 개발 과정 (1) | 2024.12.09 |
Togather : 채팅 기능 구현과 문제 해결 과정 (1) (1) | 2024.11.26 |
CoLabor : AI 법률 챗봇 파인튜닝 : 외국인 노동자를 위한 법률봇 개발 과정 (5) | 2024.11.11 |
CoLabor : 병원 데이터 파싱 및 지도 표시 기능 구현 (0) | 2024.07.19 |