Skip to content

Commit a04af24

Browse files
committed
Merge branch 'release/v1.1.0'
2 parents 70bfa7a + ed043e2 commit a04af24

34 files changed

+1082
-2
lines changed

api/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ dependencies {
1212

1313
// Swagger
1414
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3")
15+
16+
// Web Socket
17+
implementation("org.springframework.boot:spring-boot-starter-websocket")
1518
}
1619

1720
dependencyManagement {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.mbtips.common.configuration;
2+
3+
import com.mbtips.common.handler.WebSocketChatHandler;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.web.socket.config.annotation.EnableWebSocket;
7+
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
8+
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
9+
10+
@Configuration
11+
@EnableWebSocket
12+
@RequiredArgsConstructor
13+
public class WebSocketConfiguration implements WebSocketConfigurer {
14+
15+
final WebSocketChatHandler webSocketChatHandler;
16+
17+
@Override
18+
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
19+
registry.addHandler(webSocketChatHandler, "/chats");
20+
}
21+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.mbtips.common.handler;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.mbtips.common.constant.Constant;
5+
import com.mbtips.common.exception.CustomException;
6+
import com.mbtips.domain.openChat.exception.OpenChatException;
7+
import com.mbtips.openChat.application.dto.OpenChatMessageDto;
8+
import com.mbtips.openChat.application.service.OpenChatMessageService;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.stereotype.Component;
12+
import org.springframework.web.socket.CloseStatus;
13+
import org.springframework.web.socket.WebSocketMessage;
14+
import org.springframework.web.socket.WebSocketSession;
15+
import org.springframework.web.socket.handler.TextWebSocketHandler;
16+
17+
import java.io.IOException;
18+
import java.net.URLDecoder;
19+
import java.nio.charset.StandardCharsets;
20+
import java.util.*;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
23+
@Slf4j
24+
@Component
25+
@RequiredArgsConstructor
26+
public class WebSocketChatHandler extends TextWebSocketHandler {
27+
28+
private final ObjectMapper objectMapper;
29+
private final OpenChatMessageService openChatMessageService;
30+
31+
private static final String NICKNAME = "nickname";
32+
private static final String OPEN_CHAT_ID = "open_chat_id";
33+
34+
final Map<Long, Set<WebSocketSession>> webSocketSessionMap = new ConcurrentHashMap<>();
35+
36+
// 연결
37+
@Override
38+
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
39+
String query = session.getUri().getQuery();
40+
log.info("{} connected", query);
41+
42+
if (query == null) {
43+
log.error("not found Query");
44+
throw new IllegalArgumentException("Not Found Session Query");
45+
}
46+
47+
Map<String, String> queryParamMap = this.parseQueryParam(query);
48+
long openChatId = Long.parseLong(queryParamMap.get(OPEN_CHAT_ID));
49+
if (this.checkNickname(openChatId, queryParamMap.get(NICKNAME))) {
50+
throw new CustomException(OpenChatException.DUPLICATED_NICKNAME);
51+
}
52+
53+
for (Map.Entry<String, String> entry : queryParamMap.entrySet()) {
54+
if (entry.getKey().equals(OPEN_CHAT_ID)) {
55+
continue;
56+
}
57+
session.getAttributes().put(entry.getKey(), entry.getValue());
58+
}
59+
webSocketSessionMap.computeIfAbsent(openChatId, k -> new HashSet<>()).add(session);
60+
}
61+
62+
@Override
63+
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
64+
String payload = (String) message.getPayload();
65+
OpenChatMessageDto openChatMessageDto = objectMapper.readValue(payload, OpenChatMessageDto.class);
66+
Set<WebSocketSession> webSocketSessions = webSocketSessionMap.get(openChatMessageDto.openChatId());
67+
68+
if (webSocketSessions == null) {
69+
log.error("메시지를 전송할 오픈채팅방이 없습니다. openChatId : {}", openChatMessageDto.openChatId());
70+
throw new CustomException(OpenChatException.NOT_FOUND_OPEN_CHAT);
71+
}
72+
73+
openChatMessageService.send(openChatMessageDto);
74+
75+
webSocketSessions.forEach(webSocketSession -> {
76+
try {
77+
webSocketSession.sendMessage(message);
78+
} catch (IOException e) {
79+
throw new RuntimeException(e);
80+
}
81+
});
82+
}
83+
84+
@Override
85+
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
86+
87+
String query = session.getUri().getQuery();
88+
log.info("{} disconnected", query);
89+
90+
if (query == null) {
91+
log.error("not found Query");
92+
throw new IllegalArgumentException("Not Found Session Query");
93+
}
94+
95+
Map<String, String> queryParamMap = this.parseQueryParam(query);
96+
long openChatId = Long.parseLong(queryParamMap.get(OPEN_CHAT_ID));
97+
Set<WebSocketSession> webSocketSessions = webSocketSessionMap.get(openChatId);
98+
99+
if (webSocketSessions == null) {
100+
log.error("나가려는 오픈채팅방이 없습니다. openChatId : {}", openChatId);
101+
throw new CustomException(OpenChatException.NOT_FOUND_OPEN_CHAT);
102+
}
103+
webSocketSessions.removeIf(webSocketSession -> webSocketSession.getId().equals(session.getId()));
104+
}
105+
106+
private Map<String, String> parseQueryParam(String query) {
107+
HashMap<String, String> queryParamMap = new HashMap<>();
108+
String[] pairs = query.split("&");
109+
Arrays.stream(pairs).forEach(pair -> {
110+
String[] kv = pair.split("=", 2);
111+
if (kv.length == 2) {
112+
String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8);
113+
String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8);
114+
queryParamMap.put(key, value);
115+
}
116+
});
117+
return queryParamMap;
118+
}
119+
120+
private boolean checkNickname(long openChatId, String nickname) {
121+
Set<WebSocketSession> webSocketSessions = webSocketSessionMap.get(openChatId);
122+
if (webSocketSessions == null) {
123+
log.error("존재하지 않은 openChatId : {}", openChatId);
124+
throw new CustomException(OpenChatException.NOT_FOUND_OPEN_CHAT);
125+
}
126+
return webSocketSessions.stream()
127+
.anyMatch(webSocketSession -> webSocketSession.getAttributes().get(NICKNAME).equals(nickname));
128+
}
129+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.mbtips.openChat.application.dto;
2+
3+
public record OpenChatDto(
4+
String imageUrl,
5+
String title,
6+
String description
7+
) {
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.mbtips.openChat.application.dto;
2+
3+
public record OpenChatMessageDto (
4+
String nickname,
5+
String message,
6+
long openChatId
7+
) {
8+
9+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.mbtips.openChat.application.service;
2+
3+
import com.mbtips.domain.openChat.OpenChatMessage;
4+
import com.mbtips.openChat.application.dto.OpenChatMessageDto;
5+
import com.mbtips.openChat.interfaces.OpenChatMessageRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Service;
8+
9+
import java.util.List;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class OpenChatMessageService {
14+
15+
private final OpenChatMessageRepository openChatMessageRepository;
16+
17+
public long send(OpenChatMessageDto openChatMessageDto) {
18+
OpenChatMessage openChatMessage = OpenChatMessage.builder()
19+
.openChatId(openChatMessageDto.openChatId())
20+
.nickname(openChatMessageDto.nickname())
21+
.message(openChatMessageDto.message())
22+
.build();
23+
return openChatMessageRepository.save(openChatMessage);
24+
}
25+
26+
public List<OpenChatMessage> findNextOpenChatMessages(Long openChatId, Long openChatMessageId) {
27+
return openChatMessageRepository.findNextOpenChatMessages(openChatId, openChatMessageId);
28+
}
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.mbtips.openChat.application.service;
2+
3+
import com.mbtips.domain.openChat.OpenChat;
4+
import com.mbtips.openChat.application.dto.OpenChatDto;
5+
import com.mbtips.openChat.interfaces.OpenChatRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Service;
8+
9+
import java.util.List;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class OpenChatService {
14+
15+
private final OpenChatRepository openChatRepository;
16+
17+
public void save(OpenChatDto openChatDto) {
18+
OpenChat openChat = OpenChat.builder()
19+
.imageUrl(openChatDto.imageUrl())
20+
.title(openChatDto.title())
21+
.description(openChatDto.description())
22+
.isDeleted(false)
23+
.build();
24+
openChatRepository.save(openChat);
25+
}
26+
27+
public List<OpenChat> findAll() {
28+
return openChatRepository.findActiveOpenChats();
29+
}
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.mbtips.openChat.controller;
2+
3+
import com.mbtips.common.response.ApiResponse;
4+
import com.mbtips.domain.openChat.OpenChat;
5+
import com.mbtips.domain.openChat.OpenChatMessage;
6+
import com.mbtips.openChat.application.dto.OpenChatDto;
7+
import com.mbtips.openChat.application.service.OpenChatMessageService;
8+
import com.mbtips.openChat.application.service.OpenChatService;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.web.bind.annotation.*;
11+
12+
import java.util.List;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
@RequestMapping("/api/open-chat")
17+
public class OpenChatController {
18+
19+
private final OpenChatService openChatService;
20+
private final OpenChatMessageService openChatMessageService;
21+
22+
@PostMapping
23+
public ApiResponse<Void> createOpenChat(@RequestBody OpenChatDto openChatDto) {
24+
return ApiResponse.success();
25+
}
26+
27+
@GetMapping
28+
public ApiResponse<List<OpenChat>> getOpenChats() {
29+
return ApiResponse.success(openChatService.findAll());
30+
}
31+
32+
@GetMapping("/{openChatId}")
33+
public ApiResponse<List<OpenChatMessage>> getOpenChatMessages(@PathVariable("openChatId") long openChatId,
34+
@RequestParam(required = false, defaultValue = "0") long openChatMessageId) {
35+
return ApiResponse.success(openChatMessageService.findNextOpenChatMessages(openChatId, openChatMessageId));
36+
}
37+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.mbtips.domain.openChat;
2+
3+
import lombok.*;
4+
5+
@Getter
6+
@Builder
7+
@ToString
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
public class OpenChat {
11+
12+
private Long openChatId;
13+
14+
private String imageUrl;
15+
16+
private String title;
17+
18+
private String description;
19+
20+
private boolean isDeleted;
21+
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.mbtips.domain.openChat;
2+
3+
import lombok.*;
4+
5+
@Getter
6+
@Builder
7+
@ToString
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
public class OpenChatMessage {
11+
12+
private Long openChatMessageId;
13+
14+
private Long openChatId;
15+
16+
private String nickname;
17+
18+
private String mbti;
19+
20+
private String message;
21+
22+
}

0 commit comments

Comments
 (0)