Skip to content

Commit

Permalink
Merge pull request #1 from wnsrl1228/develop
Browse files Browse the repository at this point in the history
feat: 채팅 기능추가
  • Loading branch information
wnsrl1228 authored Sep 3, 2024
2 parents c44dd98 + ad8f48a commit d8accdd
Show file tree
Hide file tree
Showing 49 changed files with 639 additions and 70 deletions.
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

0 comments on commit d8accdd

Please sign in to comment.