Skip to content

Commit

Permalink
Merge pull request #3 from gyehyun-bak/feature/#2/채팅방-참가-나가기-메시지-추가하기
Browse files Browse the repository at this point in the history
feature: 채팅방 참가/나가기 시 SYSTEM 해당하는 메시지를 표시합니다.
  • Loading branch information
gyehyun-bak authored Jan 16, 2025
2 parents 7f172ee + b984777 commit db2b944
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 39 deletions.
48 changes: 17 additions & 31 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import { useEffect, useRef, useState } from "react";
import { Client } from "@stomp/stompjs";
import SockJS from "sockjs-client";

// 메시지 객체
interface MessageRequestDto {
content: string;
}

interface MessageResponseDto {
content: string;
sessionId: string;
nickname: string;
}

// 서버 웹소켓 엔드포인트트
const SOCKET_URL = "http://localhost:8080/ws";

// 닉네임 최대 길이
const MAX_NICKNAME_LENGTH = 30;
import { MessageRequestDto } from "./types/MessageRequestDto";
import { MessageResponseDto } from "./types/MessageResponseDto";
import { SOCKET_URL, MAX_NICKNAME_LENGTH } from "./config";
import SystemMessageItem from "./components/SystemMessageItem";
import ChatMessageItem from "./components/ChatMessageItem";

function App() {
const [message, setMessage] = useState("");
Expand Down Expand Up @@ -145,21 +133,19 @@ function App() {
{/* Body */}
<div className="flex-1 overflow-auto p-4">
<div className="flex flex-col gap-1">
{messages.map((message, index) => (
<div className={`flex flex-col gap-1 ${message.sessionId === sessionId ? "items-end" : "items-start"}`}>
<span className="text-sm text-neutral-400">{message.nickname}</span>
<div
{messages.map((message, index) => {
if (message.type === "SYSTEM") {
return <SystemMessageItem key={index} message={message} />;
}

return (
<ChatMessageItem
key={index}
className={`px-4 py-3 my-1 rounded-xl w-fit shadow-md ${
message.sessionId === sessionId
? "bg-blue-600 text-white self-end" // 자신의 메시지
: "bg-white self-start" // 다른 사람의 메시지
}`}
>
{message.content}
</div>
</div>
))}
message={message}
isMine={message.sessionId === sessionId}
/>
);
})}
</div>
</div>

Expand Down
26 changes: 26 additions & 0 deletions client/src/components/ChatMessageItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { MessageResponseDto } from "../types/MessageResponseDto";

interface ChatMessageItemProps {
message: MessageResponseDto;
isMine: boolean;
}

export default function ChatMessageItem({
message,
isMine,
}: ChatMessageItemProps) {
return (
<div
className={`flex flex-col gap-1 ${isMine ? "items-end" : "items-start"}`}
>
<span className="text-sm text-neutral-400">{message.nickname}</span>
<div
className={`px-4 py-3 my-1 rounded-xl w-fit shadow-md ${
isMine ? "bg-blue-600 text-white self-end" : "bg-white self-start"
}`}
>
{message.content}
</div>
</div>
);
}
15 changes: 15 additions & 0 deletions client/src/components/SystemMessageItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MessageResponseDto } from "../types/MessageResponseDto";

interface SystemMessageItemProps {
message: MessageResponseDto;
}

export default function SystemMessageItem({ message }: SystemMessageItemProps) {
return (
<div className="flex justify-center my-3">
<div className="px-4 py-2 rounded-full bg-neutral-300 text-white text-sm text-center">
{message.content}
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions client/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 서버 웹소켓 엔드포인트트
export const SOCKET_URL = "http://localhost:8080/ws";

// 닉네임 최대 길이
export const MAX_NICKNAME_LENGTH = 30;
3 changes: 3 additions & 0 deletions client/src/types/MessageRequestDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface MessageRequestDto {
content: string;
}
8 changes: 8 additions & 0 deletions client/src/types/MessageResponseDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MessageType } from "./MessageType";

export interface MessageResponseDto {
type: MessageType;
content: string;
sessionId: string;
nickname: string;
}
1 change: 1 addition & 0 deletions client/src/types/MessageType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type MessageType = "SYSTEM" | "CHAT";
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.spring_websocket.common.enums;

import org.springframework.web.socket.messaging.SessionConnectEvent;

/**
* UI 구분을 위한 메시지 타입입니다.
* <ul>
* <li>SYSTEM: 특정 사용자가 아닌 시스템에서 발행한 메시지입니다.</li>
* <li>CHAT: 사용자가 채팅을 목적으로 발행한 메시지입니다.</li>
* </ul>
*/
public enum MessageType {
SYSTEM,
CHAT
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
package com.example.spring_websocket.controller;

import com.example.spring_websocket.common.enums.MessageType;
import com.example.spring_websocket.dto.response.MessageResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@Component
@RequiredArgsConstructor
@Slf4j
public class WebSocketHandler {
/**
* 메시지를 클래스 내에서 직접 SimpleBroker로 발행하는 데 사용됩니다.
*/
private final SimpMessagingTemplate messagingTemplate;

/**
* STOMP 서브 프로토콜을 통한 CONNECT 메시지가 수신되면 발생하는 이벤트 {@link SessionConnectEvent}를 처리합니다.
* <p> STOMP CONNECT 메시지 헤더에서 nickname을 추출해 웹소켓 세션 헤더에 저장합니다.
* <ul>
* <li>STOMP CONNECT 메시지 헤더에서 nickname을 추출해 웹소켓 세션 헤더에 저장합니다.</li>
* <li>사용자가 연결되었음을 알리는 메시지를 발행합니다.</li>
* </ul>
*/
@EventListener
public void handleSessionConnect(SessionConnectEvent event) {
Expand All @@ -24,8 +36,40 @@ public void handleSessionConnect(SessionConnectEvent event) {
// 세션에 닉네임 저장
accessor.getSessionAttributes().put("nickname", nickname);

// 로깅을 위해 sessionId와 함께 보여줍니다
// 디버깅을 위해 sessionId와 nickname 을 함께 보여줍니다.
String sessionId = accessor.getSessionId();
log.info("sessionId: " + sessionId + ", nickname: " + nickname);
log.info("[SessionConnected]: sessionId = " + sessionId + ", nickname = " + nickname);

// 시스템 메시지를 생성하고 발행합니다.
MessageResponseDto responseDto = MessageResponseDto.builder()
.type(MessageType.SYSTEM)
.content(nickname + "님이 참가하였습니다.")
.build();

messagingTemplate.convertAndSend("/topic/chat", responseDto);
}

/**
* STOMP 서브 프로토콜을 사용하는 웹소켓 세션의 연결이 끊기면 발생하는 이벤트 {@link SessionDisconnectEvent}를 처리합니다.
* <ul>
* <li>사용자가 연결 해제되었음을 알리는 메시지를 발행합니다.</li>
* </ul>
*/
@EventListener
public void handleSessionDisconnect(SessionDisconnectEvent event) {
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(event.getMessage());
String nickname = (String) accessor.getSessionAttributes().get("nickname");

// 디버깅을 위해 sessionId와 nickname 을 함께 보여줍니다.
String sessionId = accessor.getSessionId();
log.info("[SessionDisconnected]: sessionId = " + sessionId + ", nickname = " + nickname);

// 시스템 메시지를 생성하고 발행합니다.
MessageResponseDto responseDto = MessageResponseDto.builder()
.type(MessageType.SYSTEM)
.content(nickname + "님이 나갔습니다.")
.build();

messagingTemplate.convertAndSend("/topic/chat", responseDto);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package com.example.spring_websocket.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import com.example.spring_websocket.common.enums.MessageType;
import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class MessageResponseDto {
private MessageType type;
private String content;
private String sessionId;
private String nickname;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.spring_websocket.service.impl;

import com.example.spring_websocket.common.enums.MessageType;
import com.example.spring_websocket.dto.request.MessageRequestDto;
import com.example.spring_websocket.dto.response.MessageResponseDto;
import com.example.spring_websocket.service.ChatService;
Expand All @@ -8,6 +9,11 @@
@Service
public class ChatServiceImpl implements ChatService {
public MessageResponseDto processMessage(MessageRequestDto requestDto, String sessionId, String nickname) {
return new MessageResponseDto(requestDto.getContent(), sessionId, nickname);
return MessageResponseDto.builder()
.type(MessageType.CHAT)
.content(requestDto.getContent())
.sessionId(sessionId)
.nickname(nickname)
.build();
}
}

0 comments on commit db2b944

Please sign in to comment.