Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 채팅 기능추가 #1

Merged
merged 16 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div align="center">

## [짤 제목 바로가기](https://memetitle.com)
<br>
<img src="https://github.com/wnsrl1228/meme-title/assets/67573836/cd3395a0-62a6-4bf7-826a-f52acef32b5c" width="15%">

<br>
<img src="https://github.com/wnsrl1228/meme-title/assets/67573836/18e4c209-b3bb-4801-a699-89f743af616d" >


<br>

</div>

<br>

## 기술 스택

### 프론트엔드

<img width="50%" src="https://github.com/wnsrl1228/meme-title/assets/67573836/c4b0adb9-0034-4049-8d08-3141de84803d">

### 백엔드

<img width="63%" src="https://github.com/wnsrl1228/meme-title/assets/67573836/e489e0f4-59f2-4031-b418-0a58ba68aacc">

### 인프라

<img width="65%" src="https://github.com/wnsrl1228/meme-title/assets/67573836/ce70a05a-0948-44f3-8605-47324db62268">

<br>

## 서비스 요청 흐름도

<img width="100%" src="https://github.com/wnsrl1228/meme-title/assets/67573836/51577ce9-6083-46f4-909e-2ab3384550e3">

## 배포 과정

<img width="100%" src="https://github.com/wnsrl1228/meme-title/assets/67573836/680b2bb6-65c2-422a-b5c6-d1ef6ed0380e">

## DB ERD
<img width="100%" src="https://github.com/wnsrl1228/meme-title/assets/67573836/8b8d9be3-7617-4bf3-a345-86c9d1a9ac42">

## API 문서
<img width="100%" src="https://github.com/wnsrl1228/meme-title/assets/67573836/0067e481-6066-44c8-8dd8-f72209eac2bd">
12 changes: 9 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.5'
id 'org.springframework.boot' version '3.2.7'
id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '11'
sourceCompatibility = '17'
}

configurations {
Expand All @@ -28,12 +28,18 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
// queryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

compileOnly 'org.projectlombok:lombok'

runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'


runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package com.memetitle.auth;

import com.memetitle.auth.dto.AdminMember;
import com.memetitle.auth.infrastructure.JwtProvider;
import com.memetitle.global.exception.AuthException;
import com.memetitle.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest;

@Slf4j
public class AdminMemberArgumentResolver implements HandlerMethodArgumentResolver {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/memetitle/auth/AuthHandlerInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;

@Slf4j
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest;

@RequiredArgsConstructor
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/memetitle/auth/domain/RefreshToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
import com.memetitle.auth.dto.response.TokenResponse;
import com.memetitle.auth.service.LoginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.java.Log;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;

import static org.springframework.http.HttpHeaders.SET_COOKIE;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.memetitle.chat.config;

import com.memetitle.chat.domain.ChatRoom;
import com.memetitle.chat.repository.ChatRoomRepository;
import com.memetitle.global.exception.InvalidException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

import static com.memetitle.global.exception.ErrorCode.NOT_FOUND_CHATROOM_ID;

@Component
@RequiredArgsConstructor
public class CustomHandshakeInterceptor implements HandshakeInterceptor {

private final ChatRoomRepository chatRoomRepository;

@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
String path = request.getURI().getPath();
String roomId = path.split("/")[2];
ChatRoom chatRoom = chatRoomRepository.findById(Long.parseLong(roomId))
.orElseThrow(() -> new InvalidException(NOT_FOUND_CHATROOM_ID));

if (chatRoom.getMemberCount() == chatRoom.getMaxCapacity()) {
return false;
}

return true;
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}
}
35 changes: 35 additions & 0 deletions src/main/java/com/memetitle/chat/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.memetitle.chat.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
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;

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final WebSocketInterceptor webSocketInterceptor;
private final CustomHandshakeInterceptor customHandshakeInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/{roomId}") // socket 연결 url
.setAllowedOrigins("https://memetitle.com", "http://localhost:3000")
.addInterceptors(customHandshakeInterceptor);
}

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub"); // 구독 url(채팅방 참여=구독)
config.setApplicationDestinationPrefixes("/pub"); // 메세지 발행 prefixes 설정
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(webSocketInterceptor);
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/memetitle/chat/config/WebSocketInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.memetitle.chat.config;

import com.memetitle.chat.repository.ChatRoomStorage;
import com.memetitle.chat.service.ChatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;

/**
* 채팅방 인원 관리용 인터셉터
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketInterceptor implements ChannelInterceptor {

private final ChatService chatService;
private final ChatRoomStorage chatRoomStorage;

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor stompHeaderAccessor = StompHeaderAccessor.wrap(message);

if (stompHeaderAccessor != null) {
StompCommand command = stompHeaderAccessor.getCommand();

switch (command) {
case SUBSCRIBE -> {
String simpDestination = (String) message.getHeaders().get("simpDestination");
String roomId = simpDestination.split("/")[3];
chatService.increaseMemberCount(Long.parseLong(roomId));

String sessionId = (String) message.getHeaders().get("simpSessionId");
chatRoomStorage.storage(sessionId, Long.parseLong(roomId));

log.info("websocket subscribe");
}
case DISCONNECT -> {
String sessionId = (String) message.getHeaders().get("simpSessionId");
Long roomId = chatRoomStorage.getRoomId(sessionId);
chatService.decreaseMemberCount(roomId);
chatRoomStorage.delete(sessionId);
log.info("websocket disconnect");
}
}
}
return message;
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/memetitle/chat/domain/ChatRoom.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.memetitle.chat.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class ChatRoom {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, length = 50)
private String name;

@Column(nullable = false)
private int memberCount;

@Column(name = "max_capacity", nullable = false)
private int maxCapacity = 10;

@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
}
28 changes: 28 additions & 0 deletions src/main/java/com/memetitle/chat/dto/ChatRoomElement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.memetitle.chat.dto;

import com.memetitle.chat.domain.ChatRoom;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class ChatRoomElement {

private Long id;
private String name;
private int memberCount;
private int maxCapacity;
private LocalDateTime createdAt;

public static ChatRoomElement of(ChatRoom chatRoom) {
return ChatRoomElement.builder()
.id(chatRoom.getId())
.name(chatRoom.getName())
.memberCount(chatRoom.getMemberCount())
.maxCapacity(chatRoom.getMaxCapacity())
.createdAt(chatRoom.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.memetitle.chat.dto.request;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class ChatMessageRequest {

private String nickname;
private String message;
private String date;

public ChatMessageRequest(String nickname, String message, String date) {
this.nickname = nickname;
this.message = message;
this.date = date;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.memetitle.chat.dto.response;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ChatMessageResponse {

private String nickname;
private String message;
private Long roomId;
private String date;

}

Loading
Loading