๋ณธ ํ๋ก์ ํธ๋ ์คํ๋ง ์น์์ผ ์ฑํ ์๋ฒ ๋ง๋ค๊ธฐ ๋ฅผ ์ค์ตํ๋ฉฐ ๊ณต๋ถํ๊ธฐ ์ํ ํ๋ก์ ํธ ์ ๋๋ค. branch ๋ณ๋ก ์ด 6๋จ๊ณ ๋ชจ๋ ์งํํ ์์ ์ ๋๋ค.
0. STOMP๋ ๋ฌด์์ธ๊ฐ?
Stomp๋ SimpleTextOrientedMessagingProtocol์ ์ฝ์๋ก, ํ ์คํธ ๊ธฐ๋ฐ ์๋ฐฉํฅ ํต์ ์ ํจ์จ์ ์ผ๋ก ํ๊ธฐ ์ํ ํต์ ๊ท์ฝ์ด๋ค.
Stomp์ ์๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ๋ค. Stomp์๋ 3๊ฐ์ง ๊ฐ๋ ์ด ์กด์ฌํ๋๋ฐ, Topic, Publisher, Subscriber๊ฐ ๊ทธ๊ฒ์ด๋ค.
Topic์ ์ฐ์ฒดํต์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค. publisher๋ ๋ฉ์ธ์ง ๋ฐํ์์ด๋ค. Publisher๊ฐ ์ด๋ค Topic์ EndPoint๋ก ์ก๊ณ ๋ฉ์ธ์ง๋ฅผ ๋ณด๋ด๋ฉด ํด๋น ์ฐ์ฒดํต์ผ๋ก ๋ฉ์ธ์ง๊ฐ ๋ค์ด๊ฐ๋ค. ์ด๋ ํด๋น Topic์ ๊ตฌ๋
ํ๊ณ ์๋ Subscriber๋ค์๊ฒ ํด๋น ๋ฉ์ธ์ง๊ฐ ์ ๋ถ ๋ฐฐ๋ฌ๋๊ฒ ๋๋ค.
์ฐ๋ฆฌ ์น ์์ผ์์๋
1. ์ ๊ฐ๋ & ์ฌ์ ์ค์
1-1 Server Logic
1-2 ์ฌ์ ์ค์
ํด๋น ํ๋ก์ ํธ๋ ๋ฐ๋ก ํ๋ก ํธ ์๋ ํ๋ก์ ํธ๋ฅผ ๋์ง ์๊ณ , SpringBoot ๋ด์์ ํ๋ก ํธ ์๋ ๋ํ ๊ตฌํํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ํ๋ฌ๊ทธ์ธ์ด ์กฐ๊ธ๋ ์ถ๊ฐ๋์๋ค. Sock.js ํ๋ฌ๊ทธ์ธ ๋ฑ์ด ํด๋น ์์ ์ ์ํ ํ๋ฌ๊ทธ์ธ์ด๋ค. ์ค์ ํ์ผ ๋ชฉ๋ก์ ๋ฐ์ ์ฐธ๊ณ ํ๋ผ.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'org.websocket'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '21'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
implementation 'org.springframework.boot:spring-boot-devtools'
implementation 'org.webjars.bower:bootstrap:4.3.1'
implementation 'org.webjars.bower:vue:2.5.16'
implementation 'org.webjars:sockjs-client:1.1.2'
implementation 'org.webjars.bower:axios:0.17.1'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
implementation 'com.google.code.gson:gson:2.8.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
}
tasks.named('test') {
useJUnitPlatform()
}
2. ์ฝ๋ ๋ถ์
2_1 ๊ณ์ธต ํ์ธ
2_2 WebSockConfig
package org.websocket.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
// ์ด์ STOMP๋ฅผ ์ด์ฉํ์ฌ WebSocket์ ๊ตฌํํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก WebSockHandler๊ฐ ํ์ ์๋ค.
// ์๋ํ๋ฉด STOMP๊ฐ ์น ์์ผ์์ ๋ฉ์ธ์ง๋ฅผ ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ์ ๋ํ ๊ท์ฝ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
@Configuration
@EnableWebSocketMessageBroker // ์น์์ผ์์ STOMP๋ฅผ ํ์ฑํ ํ๊ธฐ ์ํ ์ด๋
ธํ
์ด์
public class WebSockConfig implements WebSocketMessageBrokerConfigurer {
// ๋ฉ์ธ์ง ์ก ์์ ์ ๋ํ ์ค์ ์ ๋ฑ๋กํ ๋งค์๋์ด๋ค.
// ๋ฉ์ธ์ง ์ก์ ๊ณผ ๋ฐ์ ์ ํด๋น ์ ๋์ด๋ก ๊ตฌ๋ถํ๋ค.
// ์ฌ์ฉ์์ ๋ฉ์ธ์ง ์์ ์ฃผ์๋ sub/room/{๋ฐฉ ๋ฒํธ}๋ก ํด๋น ๋ฐฉ ๋ฒํธ๋ก ์จ ๋ฉ์ธ์ง๋ง ์์ ํ๋๋ก ์ค์
// ๋ฉ์ธ์ง ๋ฐ์ ์ ๊ฒฝ์ฐ์๋ pub/room ์ผ๋ก ๋ณด๋ด๊ณ ๋ฉ์ธ์ง ๋ณด๋ผ ๋ฐฉ ๋ฒํธ๋ responseBody ์ ๋ฉํ๋ฐ์ดํฐ๋ก ์ ์ฅํ๋ค.
@Override
public void configureMessageBroker (MessageBrokerRegistry config){
// pub๋ก ์์์ด ์ค์ ๋ ๋
์์ ๋ฐ์ ํ๋ ๋
์์ด๋ผ๊ณ ์๋ ค์ฃผ๋ ๊ฒ
config.setApplicationDestinationPrefixes("/pub");
// sub๋ก ์์ํ๋ ๋
์์ ํ์ฌ topic (์ฃผ์์ ๊ฐ์ด ์ค)์ ์ฌ๋ผ์จ ๋ฉ์ธ์ง๋ฅผ ๊ตฌ๋
ํ๋ ๋
์์ด๋ผ๊ณ ์๋ ค์ฃผ๋ ๊ฒ
config.enableSimpleBroker("/sub");
}
// Stomp ์์ผ์ ์ธ ์ฃผ์๋ฅผ ํน์ ํ๋ค.
@Override
public void registerStompEndpoints(StompEndpointRegistry registry){
// stomp websocket์ผ๋ก ์ฐ๊ฒฐํ๋ ์ฃผ์์ endpoint๋ /ws-stomp๋ก ์ค์
// ๋ฐ๋ผ์ ์ ์ฒด ์ฃผ์๋ ws://localhost:8080/ws-stomp
// ์์ผ์ ํด๋น ์ฃผ์๋ก ๋ฑ๋ก
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*")
.withSockJS();
}
}
์ด์ STOMP์ ์๋ฆฌ๋๋ก ๋ฉ์ธ์ง๋ฅผ ๋ฐ์ ํ๊ณ ์์ ํ๋ค. ๋ฐ๋ผ์ ์ฐ๋ฆฌ๋ STOMP๋ฅผ ์์ ํ์ฌ ์ธ ๊ฒ์ด๋ฏ๋ก, ๋ฐ๋ก WebSocketHandler๋ฅผ ์จ์ ์์ฒญ ์ฒ๋ฆฌ ๋ฐฉ์์ ์ ์ํ ํ์๊ฐ ์๋ค.
2_3 Controller
์ด์ ์ปจํธ๋กค๋ฌ์ ๋ํ์ฌ ์์๋ณด๊ฒ ๋ค. ์ปจํธ๋กค๋ฌ๋ ์ฑํ ์์ฒด์ ๋ํ Chat Controller์ ์ฑํ ๋ฐฉ์ ๋ํ ChatRoomController๊ฐ ์๋ค.
2_3_1 Chat Controller
๋ฉ์ธ์ง๋ฅผ ์ก์ ์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์ง์ ๋ํ ์ค๋ช ์ ์ ์ Controller์ด๋ค. ์์ธํ ์ค๋ช ์ ๊ฐ์ด ์ ์ด๋์๋ค.
// WebSocket์ผ๋ก ๋ค์ด์ค๋ ๋ฉ์ธ์ง๋ฅผ ์ฒ๋ฆฌํ๋ ์ปจํธ๋กค๋ฌ
// ๋ฐํ๊ณผ ๊ตฌ๋
์ config์์ ์ค์ ํ๋ prefix๋ก ๊ตฌ๋ถ
// -> /pub/chat/message == pub ๋ค์ ์ฃผ์๋ก ๋ฉ์ธ์ง๋ฅผ ๋ฐํํ๋ ์์ฒญ
// -> /sub/chat/message == sub ๋ค์ ์ฃผ์๋ก๋ถํฐ ๋ฐํ๋๋ ๋ฉ์ธ์ง๋ฅผ ๋ฐ์์ค๊ฒ ๋ค๋ ์์ฒญ
@RequiredArgsConstructor
@Controller
public class ChatController {
private final SimpMessageSendingOperations messagingTemplate;
// ๋ฉ์ธ์ง๊ฐ ๋ชฉ์ ์ง๋ก ์ ์ก๋๋ ๊ฒฝ์ฐ, ๋ฐ์ ๋งค์๋๊ฐ ์คํ๋๋ค.
@MessageMapping("/chat/message")
public void message (ChatMessage message) {
// ๋ง์ฝ ๋ฉ์ธ์ง ํ์
์ด ENTER ์ด๋ฉด ์
์ฅ ๋ฉ์ธ์ง๋ฅผ ๋ด์ฉ๋ฌผ๋ก ์ฒจ๋ถ
if(ChatMessage.MessageType.ENTER.equals(message.getType())) {
message.setMessage(message.getSender() + "๋์ด ์
์ฅํ์
จ์ต๋๋ค.");
}
// ๋ฉ์ธ์ง๋ฅผ ์ฑํ
๋ฐฉ ์์ ์กด์ฌํ๋ ๋ชจ๋ ์ฌ๋๋ค์๊ฒ ๋ณด๋ธ๋ค.
messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
}
2_3_2_a @MessageMapping ์ด๋ ธํ ์ด์ ์ ๋ํ์ฌ
@MessageMapping("url")์ ๊ฒฝ์ฐ, ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์ธ์ง๊ฐ ํด๋น ๋ชฉ์ ์ง๋ก ๋ค์ด์์ ๊ฒฝ์ฐ, ์ด๋ ธํ ์ด์ ๋ ๋งค์๋๋ฅผ ์คํ ์ํค๋ ์ด๋ ธํ ์ด์ ์ด๋ค.
2_3_2 chat Room Controller
์ฑํ ๋ฐฉ ์์ฑํ๊ณ , ํด๋น ์ฑํ ๋ฐฉ์ ์ ์ฅํ๊ฑฐ๋, ์กฐํํ๋ Method์ด๋ค.
// ์น ์์ผ ๋ด์ฉ ์๋๊ณ , ์ฑํ
ํ๋ฉด View ๊ตฌ์ฑ์ ์ํด ํ์ํ Controller
@RequiredArgsConstructor
@Controller
@RequestMapping ("/chat")
public class ChatRoomController {
private final ChatRoomRepository chatRoomRepository;
// ์ฑํ
๋ฆฌ์คํธ ํ๋ฉด ๋ฐํ
@GetMapping("/room")
public String rooms (Model model) {
return "/chat/room";
}
// ๋ชจ๋ ์ฑํ
๋ฐฉ ๋ชฉ๋ก ๋ฐํ
@GetMapping("/rooms")
@ResponseBody
public List<ChatRoom> room() {
return chatRoomRepository.findAllRoom();
}
// ์ฑํ
๋ฐฉ ์์ฑ -> ํ๋์ Topic์ ์์ฑ
@PostMapping("/room")
@ResponseBody
public ChatRoom createRoom(@RequestParam String name){
return chatRoomRepository.createChatRoom(name);
}
// ์ฑํ
๋ฐฉ์ ์
์ฅ -> ํด๋น ํ ํฝ์ ๊ตฌ๋
ํ๋ค๋ ๋ป
@GetMapping("/room/enter/{roomId}")
public String roomDetail (Model model, @PathVariable String roomId) {
model.addAttribute("roomId", roomId);
return "/chat/roomdetail";
}
// ํน์ ์ฑํ
๋ฐฉ ์กฐํ
@GetMapping("/room/{roomId}")
@ResponseBody
public ChatRoom roomInfo(@PathVariable String roomId) {
return chatRoomRepository.findRoomById(roomId);
}
}
3. Model
3_1 ChatRoom
chatMessage๋ ์๋์ DTO ๋ช ์ธ์ ๊ฐ์์ ๋ฐ๋ก ๊ธฐ์ ํ์ง ์์๋ค. ๋ด์ฉ์ด ์กฐ๊ธ ๋ฐ๋ ChatRoom์ ์ด์ฉํด์ ์ค๋ช ํ๊ฒ ๋ค.
/* ์ด์ ๊ณผ ๋ฌ๋ผ์ง ์ฌํญ
* 1. ์ด์ stomp์ pub/sub ๋ฐฉ์์ ์ด์ฉํ๋ฏ๋ก, ๋ฉ์ธ์ง๋ฅผ ๋ฐ๋ ์ด๊ฐ ๋๊ตฌ์ธ๊ฐ ์๋ณ์ด ๊ฐ๋ฅํ๋ค.
๋ฐ๋ผ์ chatRoom์์ ๋ฐ๋ก ๊ตฌ๋
์๋ค์ ์ธ์
์ ๋ค๊ณ ์์ ํ์๊ฐ ์๋ค.
* 2. ๋ํ ๋ฐ์ก์ ๊ตฌํ๋ stomp์ pub/sub ๋ฐฉ์์ด ์ฌ์ฉ๋๋ฏ๋ก, ์ผ์ผํ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์ธ์ง๋ฅผ ๋ฐ์กํ๋ ๊ตฌํ์ด ํ์ ์์ด์ง๋ค.
๋ฐ๋ผ์ sendMessage ๋งค์๋๋ ์ฌ๋ผ์ ธ๋ ๋๋ค.
* */
@Getter
@Setter
public class ChatRoom {
// ์ฑํ
๋ฐฉ์ ๋ฐฉ ๋ฒํธ์ ์ด๋ฆ
private String roomId;
private String name;
// ๋ฐ์ ๋ด์ฉ์ ํด๋์ค ๋งค์๋๋ก์ ํด๋์ค ์์ฑ๋ถํฐ ์กด์ฌํ๋ ๋งค์๋์ด๊ณ , ํ๋์ chatRoom์ ๋ง๋ค์ด์ ๋ฐํํ๋ค.
public static ChatRoom create(String name) {
ChatRoom chatRoom = new ChatRoom();
chatRoom.roomId = UUID.randomUUID().toString();
chatRoom.name = name;
return chatRoom;
}
}
3_1_a UUID๊ฐ ๋ฌด์์ธ๊ฐ?
UUID๋ Univarsally Unique IDentifier์ ์ฝ์๋ก ๋คํฌ์ํฌ ์์ ๊ณ ์ ํ ID๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๊ท์ฝ์ด๋ค. ์ฃผ๋ก ๋ถ์ฐ ์ปดํจํฐ ํ๊ฒฝ์์ ์ฌ์ฉ๋๋ค. ์ค์ ๊ด๋ฆฌํ ๋คํฌ์ํฌ ํ๊ฒฝ์์๋ ๊ตณ์ด ์ธ ํ์๊ฐ ์๋๋ฐ, ์ค์ ๊ด๋ฆฌํ์์๋ ์ข ์๋ ๋ชจ๋ ์ธ์ ์ ์ผ๋ จ๋ฒํธ๋ฅผ ๋ถ์ฌํด์ ์ ์ผ์ฑ์ ๋ณด์ฅํ๋ฉด ๋๋ค. ์ค์ ๊ด๋ฆฌ ์์คํ ์ด ๋ชจ๋ ์ํฉ์ ํต์ ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ด๋ค. ํ์ง๋ง ๋ถ์ฐ ํ๊ฒฝ์ผ ๊ฒฝ์ฐ ๋ก์ปฌ id๊ฐ ๋ค๋ฅธ ๋คํฌ์ํฌ์์ ์ค๋ณต๋๋ ๊ฒฝ์ฐ๋ ์์ผ๋ฏ๋ก, ์ด๋ฅผ ์ํด์ ๊ณ ์ ์ฑ์ ๋ณด์ฅํ id๊ฐ ํ๋ ๋ ํ์ ํ๋ค.
4. Repository
์ ๋ฒ 1๊ฐ์์์ Service๋ ์ง์ฐ๊ณ Repository๋ฅผ ์๋ก ๋ง๋ค์๋ค. ์์ง DB์์ ์ฐ๊ฒฐ์ ์ํ์ง๋ง, ๋ค์ ๊ฐ์ ์ํด์ ๋ง๋ ๊ฒ์ธ ๊ฑฐ ๊ฐ๋ค. DB ์ญํ ์ ์ฑํ ๋ฐฉ ์ ๋ณด ์ ์ฅ์ HashMap์ ํตํด ๋์ ํ๋ค.
@Repository
public class ChatRoomRepository {
// ์ฑํ
๋ฐฉ ์ ๋ณด๋ DB๋ฅผ ์ ๊ฑฐ์น๊ณ ๊ฐ๋จํ๊ฒ ์ ์ฅํ ๊ฒ์ด๋ฏ๋ก, Map์ผ๋ก ์ ์ฅํด๋๋ค.
private Map<String, ChatRoom> chatRoomMap;
// ChatRoomRepository๊ฐ ๋น ๊ฐ์ฒด๋ก ๋ง๋ค์ด์ง ์ดํ์, ๋ฑ ํ๋ฒ ์คํ๋๋ ํจ์์ด๋ค.
@PostConstruct
private void init() {
chatRoomMap = new LinkedHashMap<>();
}
// ์ฑํ
๋ฐฉ ์ ์ฒด ์กฐํ
public List<ChatRoom> findAllRoom() {
// ์ ์ฅ๊ณ ๋ก๋ถํฐ ๋ชจ๋ ์ฑํ
๋ฃธ์ ๋ฐ์ List์ ๋ฃ๋๋ค.
List<ChatRoom> chatRooms = new ArrayList<>(chatRoomMap.values());
// ์ฑํ
๋ฐฉ์ ์์ฑ ์์๋๋ก ๋ฐํํ๊ธฐ ์ํด ํ๋ฒ ๋ค์ง๋๋ค.
Collections.reverse(chatRooms);
return chatRooms;
}
// ๋ฐฉ ๋ฒํธ์ ๋ง๋ ๋
์ ํ๋๋ง ์ฐพ๊ธฐ
public ChatRoom findRoomById(String id) {
return chatRoomMap.get(id);
}
// ๋ฐฉ ์์ฑ
public ChatRoom createChatRoom(String name){
// DTO์ ์ ์ด๋จ๋ ์ฑํ
๋ฐฉ ์์ฑ ๋ก์ง์ ์จ์ ์ฑํ
๋ฐฉ์ ๋ง๋ ๋ค.
ChatRoom chatRoom = ChatRoom.create(name);
// ํด๋น ์ฑํ
๋ฐฉ์ ์ฑํ
๋ฐฉ์ ๋ฐฉ ๋ฒํธ๋ฅผ index๋ก ์ ์ฅ๊ณ ์ ์ ์ฅํ๋ค.
chatRoomMap.put(chatRoom.getRoomId(), chatRoom);
return chatRoom;
}
}
5. ํ๋ก ํธ ์๋ ๋ถ๋ถ
5.0 .ftl ํ์ฅ์์ ๋ํ์ฌ
.ftl์ FreeMarker file์ ์๋ฏธํ๋ฉฐ, ์๋ฐ ์๋ธ๋ฆฟ์ ์ํ ์คํ ์์ค HTML์ด๋ค. ํ๋ฆฌ๋ง์ปค๋ ํ
ํ๋ฆฟ ๊ฐ์ฒด๋ก ์ปดํ์ผ ๋๋๋ฐ, ์ด ๊ฐ์ฒด๋ ์๋ธ๋ฆฟ์์ ์ ๊ณตํ๋ ๋ฐ์ดํฐ๊ฐ ๋ฐ๋ ๋๋ง๋ค HTML์ ๋์ ์ผ๋ก ์์ฑํ๋ค.
์ฐ๋ฆฌ๋ ์์ผ์ด ์ฌ์ฉ๋๋ ๋ชจ์ต์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด .ftl ํ์ฅ์๋ฅผ ์ฌ์ฉํ์๋ค.
5.1 room.ftl
ํด๋น ํ์ผ์์ ํ ์ ์๋ ์ผ์ ๋ค์๊ณผ ๊ฐ๋ค.
- ๋ฐฉ์ ์์ฑํ๋ค.
- ์์ฑ๋ ๋ฐฉ์ ๋ฆฌ์คํธ๋ฅผ ๋ณผ ์ ์๋ค.
- ๋ฐฉ ์ค ํ๋๋ก ์ ์ฅํ ์ ์๋ค. (์ฌ๊ธฐ์ ์ ์ฅํ๊ฒ ๋๋ฉด roomDetail.ftl๋ก ์ด๋ํ๋ค.)
<!doctype html>
<html lang="en">
<head>
<title>Websocket Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- CSS -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="container" id="app" v-cloak>
<div class="row">
<div class="col-md-12">
<h3>์ฑํ
๋ฐฉ ๋ฆฌ์คํธ</h3>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">๋ฐฉ์ ๋ชฉ</label>
</div>
<input type="text" class="form-control" v-model="room_name" @keyup.enter="createRoom">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="createRoom">์ฑํ
๋ฐฉ ๊ฐ์ค</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item list-group-item-action" v-for="item in chatrooms" v-bind:key="item.roomId" v-on:click="enterRoom(item.roomId)">
{{item.name}}
</li>
</ul>
</div>
<!-- JavaScript -->
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script src="/webjars/bootstrap/4.3.1/dist/js/bootstrap.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
room_name : '',
chatrooms: [
]
},
created() {
this.findAllRoom();
},
methods: {
findAllRoom: function() {
axios.get('/chat/rooms').then(response => { this.chatrooms = response.data; });
},
createRoom: function() {
if("" === this.room_name) {
alert("๋ฐฉ ์ ๋ชฉ์ ์
๋ ฅํด ์ฃผ์ญ์์.");
return;
} else {
var params = new URLSearchParams();
params.append("name",this.room_name);
axios.post('/chat/room', params)
.then(
response => {
alert(response.data.name+"๋ฐฉ ๊ฐ์ค์ ์ฑ๊ณตํ์์ต๋๋ค.")
this.room_name = '';
this.findAllRoom();
}
)
.catch( response => { alert("์ฑํ
๋ฐฉ ๊ฐ์ค์ ์คํจํ์์ต๋๋ค."); } );
}
},
enterRoom: function(roomId) {
var sender = prompt('๋ํ๋ช
์ ์
๋ ฅํด ์ฃผ์ธ์.');
localStorage.setItem('wschat.sender',sender);
localStorage.setItem('wschat.roomId',roomId);
location.href="/chat/room/enter/"+roomId;
}
}
});
</script>
</body>
</html>
5.2 roomdetail.ftl
<!doctype html>
<html lang="en">
<head>
<title>Websocket ChatRoom</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="container" id="app" v-cloak>
<div>
<h2>{{room.name}}</h2>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">๋ด์ฉ</label>
</div>
<input type="text" class="form-control" v-model="message" @keyup.enter="sendMessage">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="sendMessage">๋ณด๋ด๊ธฐ</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item" v-for="message in messages">
{{message.sender}} - {{message.message}}</a>
</li>
</ul>
<div></div>
</div>
<!-- JavaScript -->
<script src="/webjars/vue/2.5.16/dist/vue.min.js"></script>
<script src="/webjars/axios/0.17.1/dist/axios.min.js"></script>
<script src="/webjars/bootstrap/4.3.1/dist/js/bootstrap.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3-1/stomp.min.js"></script>
<script>
// websocket & stomp initialize
var sock = new SockJS("/ws-stomp");
var ws = Stomp.over(sock);
// vue.js
var vm = new Vue({
el: '#app',
data: {
roomId: '',
room: {},
sender: '',
message: '',
messages: []
},
created() {
this.roomId = localStorage.getItem('wschat.roomId');
this.sender = localStorage.getItem('wschat.sender');
this.findRoom();
},
methods: {
findRoom: function() {
axios.get('/chat/room/'+this.roomId).then(response => { this.room = response.data; });
},
sendMessage: function() {
ws.send("/pub/chat/message", {}, JSON.stringify({type:'TALK', roomId:this.roomId, sender:this.sender, message:this.message}));
this.message = '';
},
recvMessage: function(recv) {
this.messages.unshift({"type":recv.type,"sender":recv.type=='ENTER'?'[์๋ฆผ]':recv.sender,"message":recv.message})
}
}
});
// pub/sub event
ws.connect({}, function(frame) {
ws.subscribe("/sub/chat/room/"+vm.$data.roomId, function(message) {
var recv = JSON.parse(message.body);
vm.recvMessage(recv);
});
ws.send("/pub/chat/message", {}, JSON.stringify({type:'ENTER', roomId:vm.$data.roomId, sender:vm.$data.sender}));
}, function(error) {
alert("error "+error);
});
</script>
</body>
</html>
3. ์์ฐ
(1) SpringBoot ์คํ ํ room.ftl ํ๋ฉด์ผ๋ก ์ด๋
์ผ๋จ ์๋ฐฉํฅ ํต์ ์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ํ๋ฉด์ ๋ ๊ฐ ๋์ฐ๊ฒ ๋ค.
(2) ์ผ์ชฝ์์ ์ฑํ ๋ฐฉ ๋ง๋ค๊ธฐ, ์ค๋ฅธ์ชฝ์์ ํ์ธ
(3) ์์ชฝ์์ ์ฑํ ๋ฐฉ์ ๋ค์ด๊ฐ๊ธฐ
์ด๋ฆ์ ๊ทธ๋ฅ ์์๋ก ๋ฃ์๋ค.
(4) ์ค๋ฅธ์ชฝ์์ ์ฑํ ์ณ๋ณด๊ธฐ, ์ผ์ชฝ์์๋ ๋จ๋์ง ํ์ธ
๋ค์ ์ฅ์ผ๋ก ๊ฐ๋ณผ๊ปด?