diff --git a/.github/workflows/prod-deploy-container.yml b/.github/workflows/prod-deploy-container.yml index 3824df8..c42864e 100644 --- a/.github/workflows/prod-deploy-container.yml +++ b/.github/workflows/prod-deploy-container.yml @@ -82,10 +82,9 @@ jobs: - name: Push Docker image to Docker Hub run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_IMAGE_NAME }} - # EC2 서버에 Docker Compose로 배포 - - name: Deploy to server - uses: appleboy/ssh-action@v1.1.0 - id: deploy + # SSH 비밀 키 설정 + - name: Install SSH Key + uses: webfactory/ssh-agent@v0.5.3 with: host: ${{ secrets.EC2_HOST }} username: ${{ secrets.EC2_USER }} diff --git a/build.gradle b/build.gradle index 88f2ac9..8a53a47 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { testImplementation 'com.h2database:h2' implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.webjars:stomp-websocket:2.3.4' + } tasks.named('test') { diff --git a/src/main/java/com/hyunsolution/dangu/chatlog/chatlogRepository/ChatlogRepository.java b/src/main/java/com/hyunsolution/dangu/chatlog/chatlogRepository/ChatlogRepository.java new file mode 100644 index 0000000..32d442b --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/chatlog/chatlogRepository/ChatlogRepository.java @@ -0,0 +1,18 @@ +package com.hyunsolution.dangu.chatlog.chatlogRepository; + +import com.hyunsolution.dangu.chatlog.Chatlog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ChatlogRepository extends JpaRepository { + + @Modifying + @Query( + "UPDATE Chatlog c SET c.readCount=:readCount WHERE c.user.id=:userId AND c.workspace.id=:workspaceId") + void updateCount( + @Param("userId") Long userId, + @Param("workspaceId") Long workspaceId, + @Param("readCount") int readCount); +} diff --git a/src/main/java/com/hyunsolution/dangu/chatlog/service/ChatlogService.java b/src/main/java/com/hyunsolution/dangu/chatlog/service/ChatlogService.java new file mode 100644 index 0000000..23fe682 --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/chatlog/service/ChatlogService.java @@ -0,0 +1,18 @@ +package com.hyunsolution.dangu.chatlog.service; + +import com.hyunsolution.dangu.chatlog.chatlogRepository.ChatlogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ChatlogService { + private final ChatlogRepository chatlogRepository; + + // 채팅방별 사용자의 읽은 채팅 개수 업데이트 + @Transactional + public void updateReadCount(Long chatRoomId, Long userPk, int messageCnt) { + chatlogRepository.updateCount(chatRoomId, userPk, messageCnt); + } +} diff --git a/src/main/java/com/hyunsolution/dangu/chatting/controller/ChatController.java b/src/main/java/com/hyunsolution/dangu/chatting/controller/ChatController.java index 0d1813d..d485b16 100644 --- a/src/main/java/com/hyunsolution/dangu/chatting/controller/ChatController.java +++ b/src/main/java/com/hyunsolution/dangu/chatting/controller/ChatController.java @@ -4,6 +4,8 @@ import com.hyunsolution.dangu.chatting.dto.response.ChatMessageDetailResponse; import com.hyunsolution.dangu.chatting.dto.response.ChatMessageResponse; import com.hyunsolution.dangu.chatting.service.ChatService; +import com.hyunsolution.dangu.common.apiResponse.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -11,10 +13,13 @@ import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; @Slf4j -@Controller +@RestController @RequiredArgsConstructor public class ChatController { private final ChatService chatService; @@ -33,4 +38,12 @@ public ChatMessageResponse sendChatMessage( return new ChatMessageResponse("success", response, null); } + + @Operation(summary = "채팅방 나가기", description = "채팅방을 나갈 시 해당 채팅방의 전체 메세지 개수를 저장합니다.") + @PostMapping("/chat/{chatRoomId}/exit") + public ApiResponse exitChat( + @PathVariable Long chatRoomId, @RequestHeader("Authorization") Long userPk) { + chatService.readMessageCnt(userPk, chatRoomId); + return ApiResponse.success(true); + } } diff --git a/src/main/java/com/hyunsolution/dangu/chatting/domain/ChatRepository.java b/src/main/java/com/hyunsolution/dangu/chatting/domain/ChatRepository.java index 4c42de2..209dc61 100644 --- a/src/main/java/com/hyunsolution/dangu/chatting/domain/ChatRepository.java +++ b/src/main/java/com/hyunsolution/dangu/chatting/domain/ChatRepository.java @@ -1,5 +1,11 @@ package com.hyunsolution.dangu.chatting.domain; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; -public interface ChatRepository extends JpaRepository {} +public interface ChatRepository extends JpaRepository { + + @Query("SELECT COUNT(c) FROM Chatlog c WHERE c.workspace.id=:roomId") + int countMessageByRoomId(@Param("roomId") Long roomId); +} diff --git a/src/main/java/com/hyunsolution/dangu/chatting/service/ChatService.java b/src/main/java/com/hyunsolution/dangu/chatting/service/ChatService.java index 52c4652..0ce9026 100644 --- a/src/main/java/com/hyunsolution/dangu/chatting/service/ChatService.java +++ b/src/main/java/com/hyunsolution/dangu/chatting/service/ChatService.java @@ -1,5 +1,6 @@ package com.hyunsolution.dangu.chatting.service; +import com.hyunsolution.dangu.chatlog.service.ChatlogService; import com.hyunsolution.dangu.chatting.domain.ChatRepository; import com.hyunsolution.dangu.chatting.domain.Chatting; import com.hyunsolution.dangu.chatting.dto.response.ChatMessageDetailResponse; @@ -19,6 +20,7 @@ public class ChatService { private final ChatRepository chatRepository; private final WorkspaceRepository workspaceRepository; private final UserRepository userRepository; + private final ChatlogService chatlogService; @Transactional public ChatMessageDetailResponse sendMessage(Long chatRoomId, String message, Long userPk) { @@ -39,4 +41,12 @@ public ChatMessageDetailResponse sendMessage(Long chatRoomId, String message, Lo new ChatMessageDetailResponse(message, user.getUid(), chatMessage.getCreatedAt()); return detailResponse; } + + @Transactional + public void readMessageCnt(Long chatRoomId, Long userPk) { + // 채팅방 나갈 시점에서의 메세지 개수 조회 + int messageCnt = chatRepository.countMessageByRoomId(chatRoomId); + // chatlog 테이블 속 readCount 업데이트 + chatlogService.updateReadCount(chatRoomId, userPk, messageCnt); + } } diff --git a/src/main/java/com/hyunsolution/dangu/common/config/webSocket/WebSocketSwaggerDocsController.java b/src/main/java/com/hyunsolution/dangu/common/config/webSocket/WebSocketSwaggerDocsController.java new file mode 100644 index 0000000..522b7c1 --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/common/config/webSocket/WebSocketSwaggerDocsController.java @@ -0,0 +1,20 @@ +package com.hyunsolution.dangu.common.config.webSocket; + +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WebSocketSwaggerDocsController { + @Operation( + summary = "WebSocket 연결 정보", + description = + """ + WebSocket URL, 구독 경로 및 발행 경로 안내 + - WebSocket 접속 URL: wss://54.221.244.36:8080/chat + - STOMP 구독 주소: /topic/chat/{chatRoomId} + - 메시지 전송 주소: /app/chat/{chatRoomId} + """) + @GetMapping("/websocket/info") // 스웨거 웹소켓관련 정보 제공을 위한 api + public void getWebSocketInfo() {} +} diff --git a/src/main/java/com/hyunsolution/dangu/participant/controller/ParticipantController.java b/src/main/java/com/hyunsolution/dangu/participant/controller/ParticipantController.java index 371f5cd..c8489af 100644 --- a/src/main/java/com/hyunsolution/dangu/participant/controller/ParticipantController.java +++ b/src/main/java/com/hyunsolution/dangu/participant/controller/ParticipantController.java @@ -2,6 +2,7 @@ import com.hyunsolution.dangu.common.apiResponse.ApiResponse; import com.hyunsolution.dangu.participant.domain.ParticipantRepository; +import com.hyunsolution.dangu.participant.dto.response.EnterChatRoomResponse; import com.hyunsolution.dangu.participant.service.ParticipantService; import com.hyunsolution.dangu.user.domain.UserRepository; import com.hyunsolution.dangu.workspace.domain.WorkspaceRepository; @@ -21,6 +22,7 @@ public class ParticipantController { private final WorkspaceRepository workspaceRepository; private final ParticipantService participantService; + //사용자 매칭 버튼 클릭 @PostMapping("/participant/matching/{roomNumber}") public ApiResponse matching( @Parameter(hidden = true) @RequestHeader("Authorization") Long id, @@ -30,4 +32,13 @@ public ApiResponse matching( participantService.changeMatching(id, workspaceId); return ApiResponse.success(null); } + + // 사용자 채팅방 입장 + @PostMapping("/participant/{roomNumber}") + public ApiResponse enterChatRoom( + @Parameter(hidden = true) @RequestHeader("Authorization") Long id, + @PathVariable("roomNumber") Long workspaceId) { + EnterChatRoomResponse response = participantService.sendEnteringMessage(id, workspaceId); + return ApiResponse.success(response); + } } diff --git a/src/main/java/com/hyunsolution/dangu/participant/domain/ParticipantRepository.java b/src/main/java/com/hyunsolution/dangu/participant/domain/ParticipantRepository.java index 764775a..3f8256c 100644 --- a/src/main/java/com/hyunsolution/dangu/participant/domain/ParticipantRepository.java +++ b/src/main/java/com/hyunsolution/dangu/participant/domain/ParticipantRepository.java @@ -21,4 +21,6 @@ public interface ParticipantRepository extends JpaRepository "SELECT p from Participant p join p.workspace w where p.user.id =:id and p.workspace.id=:workspaceId") Optional findByUserIdAndWorkspaceId( @Param("id") Long id, @Param("workspaceId") Long workspaceId); + + List findByWorkspaceId(Long workspaceId); } diff --git a/src/main/java/com/hyunsolution/dangu/participant/dto/response/EnterChatRoomResponse.java b/src/main/java/com/hyunsolution/dangu/participant/dto/response/EnterChatRoomResponse.java new file mode 100644 index 0000000..9200048 --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/participant/dto/response/EnterChatRoomResponse.java @@ -0,0 +1,12 @@ +package com.hyunsolution.dangu.participant.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class EnterChatRoomResponse { + private String message; +} diff --git a/src/main/java/com/hyunsolution/dangu/participant/exception/ParticipantError.java b/src/main/java/com/hyunsolution/dangu/participant/exception/ParticipantError.java new file mode 100644 index 0000000..58605a2 --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/participant/exception/ParticipantError.java @@ -0,0 +1,22 @@ +package com.hyunsolution.dangu.participant.exception; + +import com.hyunsolution.dangu.common.exception.BaseErrorCode; +import com.hyunsolution.dangu.common.exception.ExceptionDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ParticipantError implements BaseErrorCode { + PARTICIPANT_NOT_FOUND("PARTICIPANT_400_1", HttpStatus.BAD_REQUEST, "해당 채팅방 참여자를 찾을 수 없습니다."); + + private final String code; + private final HttpStatus httpStatus; + private final String message; + + @Override + public ExceptionDto getErrorReason() { + return ExceptionDto.builder().code(code).message(message).httpStatus(httpStatus).build(); + } +} diff --git a/src/main/java/com/hyunsolution/dangu/participant/exception/ParticipantNotFoundException.java b/src/main/java/com/hyunsolution/dangu/participant/exception/ParticipantNotFoundException.java new file mode 100644 index 0000000..bed8a01 --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/participant/exception/ParticipantNotFoundException.java @@ -0,0 +1,11 @@ +package com.hyunsolution.dangu.participant.exception; + +import com.hyunsolution.dangu.common.exception.CustomException; + +public class ParticipantNotFoundException extends CustomException { + public static final ParticipantNotFoundException EXCEPTION = new ParticipantNotFoundException(); + + private ParticipantNotFoundException() { + super(ParticipantError.PARTICIPANT_NOT_FOUND); + } +} diff --git a/src/main/java/com/hyunsolution/dangu/participant/service/ParticipantService.java b/src/main/java/com/hyunsolution/dangu/participant/service/ParticipantService.java index deefe78..5c1f04e 100644 --- a/src/main/java/com/hyunsolution/dangu/participant/service/ParticipantService.java +++ b/src/main/java/com/hyunsolution/dangu/participant/service/ParticipantService.java @@ -2,11 +2,14 @@ import com.hyunsolution.dangu.participant.domain.Participant; import com.hyunsolution.dangu.participant.domain.ParticipantRepository; +import com.hyunsolution.dangu.participant.dto.response.EnterChatRoomResponse; +import com.hyunsolution.dangu.participant.exception.ParticipantNotFoundException; +import com.hyunsolution.dangu.user.domain.User; import com.hyunsolution.dangu.user.domain.UserRepository; import com.hyunsolution.dangu.workspace.domain.Workspace; import com.hyunsolution.dangu.workspace.domain.WorkspaceRepository; import java.util.List; -import java.util.NoSuchElementException; +import com.hyunsolution.dangu.workspace.exception.WorkspaceNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,27 +27,48 @@ public void changeMatching(Long id, Long workspaceId) { Participant participantOptional = participantRepository .findByUserIdAndWorkspaceId(id, workspaceId) - .orElseThrow(() -> new NoSuchElementException("Participant not found")); + .orElseThrow(() -> ParticipantNotFoundException.EXCEPTION); participantOptional.accept(); // participant테이블에서 roomNumber로 들어온 숫자를 통해 누가 있는지 파악 - List participantIds = - participantRepository.findParticipantIdByWorkspaceId(workspaceId); + List participantIds = participantRepository.findParticipantIdByWorkspaceId(workspaceId); // 방안에 모든 참가자가 "확정"버튼을 눌렀는지 확인 for (Long participant : participantIds) { - boolean mathingCheck = - participantRepository.existsByIdAndParticipantMatchTrue(participant); + boolean mathingCheck = participantRepository.existsByIdAndParticipantMatchTrue(participant); if (!mathingCheck) { return; } } // 게임방 테이블 속 매칭 결과를 true로 바꿈 - Workspace workspace1 = - workspaceRepository - .findById(workspaceId) - .orElseThrow(() -> new NoSuchElementException("Workspace not found")); + Workspace workspace1 = workspaceRepository.findById(workspaceId).orElseThrow(()-> WorkspaceNotFoundException.EXCEPTION); workspace1.acceptFinal(); } + + // 채팅방 입장 메시지 전송 + public EnterChatRoomResponse sendEnteringMessage(Long id, Long workspaceId) { + User user = userRepository.findById(id).orElseThrow(); + Workspace workspace = workspaceRepository.findById(workspaceId).orElseThrow(); + String message; + // 기존 채팅방에 1명(방장)만 존재하며 & 입장자가 방장이 아닐때 입장 메시지를 보낸다. + if (participantRepository.findByWorkspaceId(workspaceId).size() != 2 + && workspaceRepository.findById(workspaceId).get().getCreator().getId() != id) { + + // 입장자 participant 테이블에 저장 + Participant participant = + Participant.builder() + .user(user) + .workspace(workspace) + .participantMatch(false) + .build(); + participantRepository.save(participant); + + String userName = userRepository.findById(id).get().getUid(); + message = userName + "님이 입장하셨습니다."; + } else { + message = ""; + } + return new EnterChatRoomResponse(message); + } } diff --git a/src/main/java/com/hyunsolution/dangu/user/domain/UserRepository.java b/src/main/java/com/hyunsolution/dangu/user/domain/UserRepository.java index d4383dd..e1d05ec 100644 --- a/src/main/java/com/hyunsolution/dangu/user/domain/UserRepository.java +++ b/src/main/java/com/hyunsolution/dangu/user/domain/UserRepository.java @@ -1,8 +1,9 @@ package com.hyunsolution.dangu.user.domain; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { Optional findByUid(String uid); } diff --git a/src/main/java/com/hyunsolution/dangu/workspace/domain/Workspace.java b/src/main/java/com/hyunsolution/dangu/workspace/domain/Workspace.java index 4f9d3cb..42d38f7 100644 --- a/src/main/java/com/hyunsolution/dangu/workspace/domain/Workspace.java +++ b/src/main/java/com/hyunsolution/dangu/workspace/domain/Workspace.java @@ -2,7 +2,6 @@ import com.hyunsolution.dangu.participant.domain.Participant; import com.hyunsolution.dangu.user.domain.User; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import javax.persistence.*; @@ -14,6 +13,8 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/com/hyunsolution/dangu/workspace/exception/WorkspaceError.java b/src/main/java/com/hyunsolution/dangu/workspace/exception/WorkspaceError.java new file mode 100644 index 0000000..34c5026 --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/workspace/exception/WorkspaceError.java @@ -0,0 +1,22 @@ +package com.hyunsolution.dangu.workspace.exception; + +import com.hyunsolution.dangu.common.exception.BaseErrorCode; +import com.hyunsolution.dangu.common.exception.ExceptionDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum WorkspaceError implements BaseErrorCode { + WORKSPACE_NOT_FOUND("WORKSAPCE_400_1", HttpStatus.BAD_REQUEST, "채팅방을 찾을 수 없습니다."); + + private final String code; + private final HttpStatus httpStatus; + private final String message; + + @Override + public ExceptionDto getErrorReason() { + return ExceptionDto.builder().code(code).message(message).httpStatus(httpStatus).build(); + } +} diff --git a/src/main/java/com/hyunsolution/dangu/workspace/exception/WorkspaceNotFoundException.java b/src/main/java/com/hyunsolution/dangu/workspace/exception/WorkspaceNotFoundException.java new file mode 100644 index 0000000..dce40aa --- /dev/null +++ b/src/main/java/com/hyunsolution/dangu/workspace/exception/WorkspaceNotFoundException.java @@ -0,0 +1,11 @@ +package com.hyunsolution.dangu.workspace.exception; + +import com.hyunsolution.dangu.common.exception.CustomException; + +public class WorkspaceNotFoundException extends CustomException { + public static final WorkspaceNotFoundException EXCEPTION = new WorkspaceNotFoundException(); + + private WorkspaceNotFoundException() { + super(WorkspaceError.WORKSPACE_NOT_FOUND); + } +} diff --git a/src/main/java/com/hyunsolution/dangu/workspace/service/WorkspaceService.java b/src/main/java/com/hyunsolution/dangu/workspace/service/WorkspaceService.java index 1603c72..f6c1330 100644 --- a/src/main/java/com/hyunsolution/dangu/workspace/service/WorkspaceService.java +++ b/src/main/java/com/hyunsolution/dangu/workspace/service/WorkspaceService.java @@ -9,8 +9,11 @@ import com.hyunsolution.dangu.workspace.domain.Workspace; import com.hyunsolution.dangu.workspace.domain.WorkspaceRepository; import com.hyunsolution.dangu.workspace.dto.response.GetWorkspacesResponse; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,13 +37,8 @@ public List getWorkspaces() { LocalDateTime dateFilter = LocalDateTime.now().minusDays(1); return workSpaceRepository.findAll().stream() .filter(workspace -> !workspace.isMatched()) - .filter( - workspace -> - workspace.getCreatedAt().isAfter(dateFilter)) // 게임방 조회: 유지 시간은 24h - .map( - workspace -> - GetWorkspacesResponse.of( - workspace.getId(), workspace.getCreator().getUid())) + .filter(workspace -> workspace.getCreatedAt().isAfter(dateFilter)) //게임방 조회: 유지 시간은 24h + .map(workspace ->GetWorkspacesResponse.of(workspace.getId(), workspace.getCreator().getUid())) .toList(); } }