Skip to content

Commit 1db04a0

Browse files
authored
Merge pull request #74 from kjiyun/chat
#17 Feat: 실시간 채팅 전송, 조회 기능 추가
2 parents 1257dbe + be24627 commit 1db04a0

File tree

10 files changed

+148
-27
lines changed

10 files changed

+148
-27
lines changed
Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package com.memesphere.domain.chat.controller;
22

33
import com.memesphere.domain.chat.dto.request.ChatRequest;
4+
import com.memesphere.domain.chat.dto.response.ChatListResponse;
45
import com.memesphere.domain.chat.dto.response.ChatResponse;
56
import com.memesphere.domain.chat.service.ChatService;
67
import com.memesphere.global.apipayload.ApiResponse;
8+
import com.memesphere.global.jwt.CustomUserDetails;
79
import io.swagger.v3.oas.annotations.Operation;
810
import io.swagger.v3.oas.annotations.tags.Tag;
911
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.log4j.Log4j2;
1013
import org.springframework.messaging.handler.annotation.DestinationVariable;
1114
import org.springframework.messaging.handler.annotation.MessageMapping;
1215
import org.springframework.messaging.handler.annotation.Payload;
1316
import org.springframework.messaging.handler.annotation.SendTo;
14-
import org.springframework.web.bind.annotation.GetMapping;
15-
import org.springframework.web.bind.annotation.PathVariable;
16-
import org.springframework.web.bind.annotation.RestController;
17+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
18+
import org.springframework.web.bind.annotation.*;
1719

1820
@Tag(name = "실시간 채팅", description = "실시간 채팅 관련 API")
1921
@RestController
@@ -22,16 +24,25 @@ public class ChatController {
2224

2325
private final ChatService chatService;
2426

25-
@MessageMapping("/chat/{coin_id}")
26-
@SendTo("/sub/{coin_id}")
27+
@MessageMapping("/chat/{coin_id}") // 클라이언트가 pub/chat/{coin_id}로 STOMP 메시지를 전송하면 실행됨
28+
@SendTo("/sub/{coin_id}") // 메서드가 반환한 데이터를 구독 중인 클라이언트에게 전송
2729
public ChatResponse chat(@DestinationVariable("coin_id") Long coin_id,
2830
@Payload ChatRequest chatRequest) {
2931

3032
return chatService.saveMessage(coin_id, chatRequest);
3133
}
3234

35+
@GetMapping("/chat/{coin_id}/list/")
36+
@Operation(summary = "코인별 채팅 전체 메시지 조회 API",
37+
description = "특정 코인의 채팅방의 전체 메시지를 보여줍니다.")
38+
public ApiResponse<ChatListResponse> getChatList(@PathVariable("coin_id") Long coin_id) {
39+
ChatListResponse chatListResponse = chatService.getChatList(coin_id);
40+
41+
return ApiResponse.onSuccess(chatListResponse);
42+
}
43+
3344
//최신 댓글 조회 Api
34-
@GetMapping("/latest/{coin_id}")
45+
@GetMapping("/chat/{coin_id}/latest/")
3546
@Operation(summary = "코인별 최신 댓글 조회 API",
3647
description = "특정 코인에 대한 최신 댓글을 반환합니다. 요청 시 최신 댓글 하나만 가져옵니다.")
3748
public ApiResponse<ChatResponse> getLatestMessages(
@@ -42,4 +53,16 @@ public ApiResponse<ChatResponse> getLatestMessages(
4253

4354
return ApiResponse.onSuccess(latestMessage);
4455
}
56+
57+
@PostMapping("/chat/{coin_id}/like")
58+
@Operation(summary = "채팅 좋아요 관련 API",
59+
description = "채팅에서 하트를 클릭 시 하트가 채워지고, 좋아요 누른 사람의 숫자에 반영됩니다.")
60+
public ApiResponse<String> postChatLike(@PathVariable("coin_id") Long coin_id,
61+
@RequestParam(value = "chat_id", required = true) Long chat_id,
62+
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
63+
64+
Long userId = customUserDetails.getUser().getId();
65+
66+
return ApiResponse.onSuccess(chatService.postChatLike(chat_id, userId));
67+
}
4568
}

src/main/java/com/memesphere/domain/chat/converter/ChatConverter.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package com.memesphere.domain.chat.converter;
22

3+
import com.memesphere.domain.chat.dto.response.ChatListResponse;
34
import com.memesphere.domain.chat.entity.Chat;
5+
import com.memesphere.domain.chat.entity.ChatLike;
46
import com.memesphere.domain.memecoin.entity.MemeCoin;
57
import com.memesphere.domain.chat.dto.request.ChatRequest;
68
import com.memesphere.domain.chat.dto.response.ChatResponse;
9+
import com.memesphere.domain.user.entity.User;
10+
11+
import java.util.List;
712

813
public class ChatConverter {
914

10-
public static Chat toChat(MemeCoin memeCoin, ChatRequest chatRequest) {
15+
public static Chat toChat(MemeCoin memeCoin, ChatRequest chatRequest, User user) {
1116

1217
return Chat.builder()
13-
// .user(user)
18+
.user(user)
1419
.memeCoin(memeCoin)
1520
.message(chatRequest.getMessage())
1621
.build();
@@ -27,4 +32,17 @@ public static ChatResponse toChatResponse(Chat chat) {
2732
.nickname(chat.getUser().getNickname())
2833
.build();
2934
}
35+
36+
public static ChatListResponse toChatListResponse(List<ChatResponse> chatResponses) {
37+
return ChatListResponse.builder()
38+
.chats(chatResponses)
39+
.build();
40+
}
41+
42+
public static ChatLike toChatLike(Chat chat, User user) {
43+
return ChatLike.builder()
44+
.user(user)
45+
.chat(chat)
46+
.build();
47+
}
3048
}

src/main/java/com/memesphere/domain/chat/dto/request/ChatRequest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
@NoArgsConstructor
1111
public class ChatRequest {
1212

13-
@Schema(description = "메시지 내용")
13+
@Schema(description = "닉네임", example = "코인전문가")
14+
private String nickname;
15+
16+
@Schema(description = "메시지 내용", example = "도지코인 현재 얼마인가요?")
1417
private String message;
1518
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.memesphere.domain.chat.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
@Builder
11+
public class ChatListResponse {
12+
13+
@Schema(description = "채팅 리스트")
14+
private List<ChatResponse> chats;
15+
}

src/main/java/com/memesphere/domain/chat/entity/Chat.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class Chat extends BaseEntity {
2525
@Column(name="chat_id")
2626
private Long id;
2727

28-
@Column
28+
@Column(columnDefinition = "varchar(500)", nullable = false)
2929
private String message;
3030

3131
@ManyToOne(fetch=FetchType.LAZY)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.memesphere.domain.chat.repository;
2+
3+
import com.memesphere.domain.chat.entity.Chat;
4+
import com.memesphere.domain.chat.entity.ChatLike;
5+
import com.memesphere.domain.user.entity.User;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
8+
import java.util.Optional;
9+
10+
public interface ChatLikeRepository extends JpaRepository<ChatLike, Long> {
11+
12+
Optional<ChatLike> findByChatAndUser(Chat chat, User user);
13+
}

src/main/java/com/memesphere/domain/chat/repository/ChatRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import com.memesphere.domain.chat.entity.Chat;
44
import org.springframework.data.jpa.repository.JpaRepository;
55

6+
import java.util.List;
7+
68
public interface ChatRepository extends JpaRepository<Chat, Long>, ChatCustomRepository {
79

810
Chat findFirstByMemeCoin_IdOrderByCreatedAtDesc(Long coin_id);
11+
List<Chat> findAllByMemeCoin_Id(Long coin_id);
912
}

src/main/java/com/memesphere/domain/chat/service/ChatService.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package com.memesphere.domain.chat.service;
22

33
import com.memesphere.domain.chat.converter.ChatConverter;
4+
import com.memesphere.domain.chat.dto.response.ChatListResponse;
45
import com.memesphere.domain.chat.entity.Chat;
6+
import com.memesphere.domain.chat.entity.ChatLike;
7+
import com.memesphere.domain.chat.repository.ChatLikeRepository;
8+
import com.memesphere.domain.user.entity.User;
9+
import com.memesphere.domain.user.repository.UserRepository;
510
import com.memesphere.global.apipayload.code.status.ErrorStatus;
611
import com.memesphere.global.apipayload.exception.GeneralException;
712
import com.memesphere.domain.memecoin.entity.MemeCoin;
@@ -11,29 +16,54 @@
1116
import com.memesphere.domain.chat.repository.ChatRepository;
1217
import lombok.RequiredArgsConstructor;
1318
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Optional;
1424

1525
@Service
1626
@RequiredArgsConstructor
1727
public class ChatService {
1828

1929
private final MemeCoinRepository memeCoinRepository;
2030
private final ChatRepository chatRepository;
31+
private final UserRepository userRepository;
32+
private final ChatLikeRepository chatLikeRepository;
2133

2234
public ChatResponse saveMessage(Long coin_id, ChatRequest chatRequest) {
2335

2436
MemeCoin memeCoin = memeCoinRepository.findById(coin_id)
2537
.orElseThrow(() -> new GeneralException(ErrorStatus.MEMECOIN_NOT_FOUND));
2638

27-
Chat chat = ChatConverter.toChat(memeCoin, chatRequest);
39+
User user = userRepository.findByNickname(chatRequest.getNickname())
40+
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));
41+
42+
Chat chat = ChatConverter.toChat(memeCoin, chatRequest, user);
2843
Chat savedChat = chatRepository.save(chat);
2944

3045
return ChatConverter.toChatResponse(savedChat);
3146
}
3247

48+
@Transactional
49+
public ChatListResponse getChatList(Long coin_id) {
50+
51+
List<Chat> chatList = chatRepository.findAllByMemeCoin_Id(coin_id);
52+
List<ChatResponse> chatResponses = new ArrayList<>();
53+
54+
for (Chat chat : chatList) {
55+
ChatResponse chatResponse = ChatConverter.toChatResponse(chat);
56+
chatResponses.add(chatResponse);
57+
}
58+
59+
ChatListResponse chatListResponse = ChatConverter.toChatListResponse(chatResponses);
60+
return chatListResponse;
61+
}
62+
3363
// 최신 댓글을 가져오는 메서드
64+
@Transactional
3465
public ChatResponse getLatestMessages(Long coin_id) {
3566

36-
3767
MemeCoin memeCoin = memeCoinRepository.findById(coin_id)
3868
//id에 해당하는 밈코인 없을 때
3969
.orElseThrow(() -> new GeneralException(ErrorStatus.MEMECOIN_NOT_FOUND));
@@ -50,4 +80,27 @@ public ChatResponse getLatestMessages(Long coin_id) {
5080
return ChatConverter.toChatResponse(latestChat);
5181
}
5282

83+
@Transactional
84+
public String postChatLike(Long chat_id, Long user_id) {
85+
86+
User user = userRepository.findById(user_id)
87+
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));
88+
89+
Chat chat = chatRepository.findById(chat_id)
90+
.orElseThrow(() -> new GeneralException(ErrorStatus.CHAT_NOT_FOUND));
91+
92+
// 사용자가 좋아요 눌렀는지 확인
93+
Optional<ChatLike> existingLike = chatLikeRepository.findByChatAndUser(chat, user);
94+
System.out.println("좋아요" + existingLike.isPresent());
95+
96+
if (existingLike.isPresent()) {
97+
chatLikeRepository.delete(existingLike.get());
98+
return "좋아요를 취소했습니다.";
99+
} else {
100+
ChatLike chatLike = ChatConverter.toChatLike(chat, user);
101+
chatLikeRepository.save(chatLike);
102+
return "좋아요를 눌렀습니다.";
103+
}
104+
}
105+
53106
}

src/main/java/com/memesphere/global/apipayload/code/status/ErrorStatus.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ public enum ErrorStatus implements BaseCode {
4545

4646
// 이미지 에러
4747
INVALID_FILE_EXTENTION(HttpStatus.BAD_REQUEST, "INVALID FILE EXTENSION", "지원되지 않는 파일 형식입니다."),
48-
PRESIGNED_URL_FAILED(HttpStatus.BAD_REQUEST, "PRESIGNED URL GENERATION FAILED", "presigned URL 생성에 실패했습니다.");
48+
PRESIGNED_URL_FAILED(HttpStatus.BAD_REQUEST, "PRESIGNED URL GENERATION FAILED", "presigned URL 생성에 실패했습니다."),
49+
50+
// 채팅 에러
51+
CHAT_NOT_FOUND(HttpStatus.NOT_FOUND, "CHAT NOT FOUND", "채팅을 찾을 수 없습니다.");
4952

5053
private final HttpStatus httpStatus;
5154
private final String code;
Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package com.memesphere.global.config;
22

3-
import org.springframework.context.annotation.Bean;
43
import org.springframework.context.annotation.Configuration;
54
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
6-
import org.springframework.web.socket.WebSocketHandler;
75
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
86
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
97
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@@ -19,11 +17,8 @@ public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
1917
*/
2018
@Override
2119
public void registerStompEndpoints(StompEndpointRegistry registry) {
22-
registry.addEndpoint("/ws")
23-
.setAllowedOrigins("http://localhost:8080")
24-
.withSockJS(); // ws, wss 대신 http, https를 통해 웹 소켓 연결하도록 함
25-
registry.addEndpoint("/connection")
26-
.setAllowedOriginPatterns("*");
20+
registry.addEndpoint("/ws") //socket 연결 url: ws://localhost:8008/ws
21+
.setAllowedOrigins("*");
2722
}
2823

2924
/*
@@ -33,14 +28,9 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {
3328
public void configureMessageBroker(MessageBrokerRegistry registry) {
3429

3530
//sub 으로 시작되는 요청을 구독한 모든 사용자들에게 메시지를 broadcast한다.
36-
registry.enableSimpleBroker("/sub");
31+
registry.enableSimpleBroker("/sub"); // 구독 url
3732
//pub로 시작되는 메시지는 message-handling method로 라우팅된다.
3833
//클라이언트가 서버로 전송하는 메세지의 경로 앞에 /pub 붙임
39-
registry.setApplicationDestinationPrefixes("/pub");
34+
registry.setApplicationDestinationPrefixes("/pub"); // prefix 정의
4035
}
41-
42-
// @Bean
43-
// public WebSocketHandler webSocketHandler() {
44-
// return new WebSocketHandler();
45-
// }
4636
}

0 commit comments

Comments
 (0)