diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dbfc7c1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+
+
+## [짤 제목 바로가기](https://memetitle.com)
+
+

+
+
+

+
+
+
+
+
+
+
+
+## 기술 스택
+
+### 프론트엔드
+
+
+
+### 백엔드
+
+
+
+### 인프라
+
+
+
+
+
+## 서비스 요청 흐름도
+
+
+
+## 배포 과정
+
+
+
+## DB ERD
+
+
+## API 문서
+
diff --git a/build.gradle b/build.gradle
index 1bde523..45ec24c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,6 @@
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'
}
@@ -8,7 +8,7 @@ group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
- sourceCompatibility = '11'
+ sourceCompatibility = '17'
}
configurations {
@@ -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'
diff --git a/src/main/java/com/memetitle/auth/AdminMemberArgumentResolver.java b/src/main/java/com/memetitle/auth/AdminMemberArgumentResolver.java
index af7cb82..464feda 100644
--- a/src/main/java/com/memetitle/auth/AdminMemberArgumentResolver.java
+++ b/src/main/java/com/memetitle/auth/AdminMemberArgumentResolver.java
@@ -1,10 +1,8 @@
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;
@@ -12,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;
@Slf4j
public class AdminMemberArgumentResolver implements HandlerMethodArgumentResolver {
diff --git a/src/main/java/com/memetitle/auth/AuthHandlerInterceptor.java b/src/main/java/com/memetitle/auth/AuthHandlerInterceptor.java
index efb471e..d3e1c4b 100644
--- a/src/main/java/com/memetitle/auth/AuthHandlerInterceptor.java
+++ b/src/main/java/com/memetitle/auth/AuthHandlerInterceptor.java
@@ -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
diff --git a/src/main/java/com/memetitle/auth/LoginMemberArgumentResolver.java b/src/main/java/com/memetitle/auth/LoginMemberArgumentResolver.java
index f3bf1ce..f328854 100644
--- a/src/main/java/com/memetitle/auth/LoginMemberArgumentResolver.java
+++ b/src/main/java/com/memetitle/auth/LoginMemberArgumentResolver.java
@@ -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 {
diff --git a/src/main/java/com/memetitle/auth/domain/RefreshToken.java b/src/main/java/com/memetitle/auth/domain/RefreshToken.java
index 6d47443..e341bc8 100644
--- a/src/main/java/com/memetitle/auth/domain/RefreshToken.java
+++ b/src/main/java/com/memetitle/auth/domain/RefreshToken.java
@@ -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
diff --git a/src/main/java/com/memetitle/auth/infrastructure/JwtProvider.java b/src/main/java/com/memetitle/auth/infrastructure/JwtProvider.java
index 4d75955..48bddcc 100644
--- a/src/main/java/com/memetitle/auth/infrastructure/JwtProvider.java
+++ b/src/main/java/com/memetitle/auth/infrastructure/JwtProvider.java
@@ -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;
diff --git a/src/main/java/com/memetitle/auth/presentation/LoginController.java b/src/main/java/com/memetitle/auth/presentation/LoginController.java
index 36aa734..7415276 100644
--- a/src/main/java/com/memetitle/auth/presentation/LoginController.java
+++ b/src/main/java/com/memetitle/auth/presentation/LoginController.java
@@ -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;
diff --git a/src/main/java/com/memetitle/chat/config/CustomHandshakeInterceptor.java b/src/main/java/com/memetitle/chat/config/CustomHandshakeInterceptor.java
new file mode 100644
index 0000000..f192338
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/config/CustomHandshakeInterceptor.java
@@ -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 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) {}
+}
diff --git a/src/main/java/com/memetitle/chat/config/WebSocketConfig.java b/src/main/java/com/memetitle/chat/config/WebSocketConfig.java
new file mode 100644
index 0000000..1c81180
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/config/WebSocketConfig.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/memetitle/chat/config/WebSocketInterceptor.java b/src/main/java/com/memetitle/chat/config/WebSocketInterceptor.java
new file mode 100644
index 0000000..8d9ce9e
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/config/WebSocketInterceptor.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/memetitle/chat/domain/ChatRoom.java b/src/main/java/com/memetitle/chat/domain/ChatRoom.java
new file mode 100644
index 0000000..1dae452
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/domain/ChatRoom.java
@@ -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;
+}
diff --git a/src/main/java/com/memetitle/chat/dto/ChatRoomElement.java b/src/main/java/com/memetitle/chat/dto/ChatRoomElement.java
new file mode 100644
index 0000000..91c6676
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/dto/ChatRoomElement.java
@@ -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();
+ }
+}
diff --git a/src/main/java/com/memetitle/chat/dto/request/ChatMessageRequest.java b/src/main/java/com/memetitle/chat/dto/request/ChatMessageRequest.java
new file mode 100644
index 0000000..6b13f87
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/dto/request/ChatMessageRequest.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/memetitle/chat/dto/response/ChatMessageResponse.java b/src/main/java/com/memetitle/chat/dto/response/ChatMessageResponse.java
new file mode 100644
index 0000000..ea0bbac
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/dto/response/ChatMessageResponse.java
@@ -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;
+
+}
+
diff --git a/src/main/java/com/memetitle/chat/dto/response/ChatRoomsResponse.java b/src/main/java/com/memetitle/chat/dto/response/ChatRoomsResponse.java
new file mode 100644
index 0000000..ff61a6e
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/dto/response/ChatRoomsResponse.java
@@ -0,0 +1,24 @@
+package com.memetitle.chat.dto.response;
+
+import com.memetitle.chat.domain.ChatRoom;
+import com.memetitle.chat.dto.ChatRoomElement;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.List;
+
+@Getter
+@Builder
+public class ChatRoomsResponse {
+ private List chatRooms;
+
+ public static ChatRoomsResponse ofChatRooms(List chatRooms) {
+ final List chatRoomElements = chatRooms.stream()
+ .map(ChatRoomElement::of)
+ .toList();
+
+ return ChatRoomsResponse.builder()
+ .chatRooms(chatRoomElements)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/memetitle/chat/presentation/ChatController.java b/src/main/java/com/memetitle/chat/presentation/ChatController.java
new file mode 100644
index 0000000..536af13
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/presentation/ChatController.java
@@ -0,0 +1,45 @@
+package com.memetitle.chat.presentation;
+
+import com.memetitle.chat.dto.request.ChatMessageRequest;
+import com.memetitle.chat.dto.response.ChatMessageResponse;
+import com.memetitle.chat.dto.response.ChatRoomsResponse;
+import com.memetitle.chat.service.ChatService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.messaging.handler.annotation.DestinationVariable;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.SendTo;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+public class ChatController {
+
+ private final ChatService chatService;
+ /**
+ * 클라이언트에서 메세지 요청시 : /pub/chat/message
+ * 채팅방 사람들에게 메세지 전달 : /sub/chat/messages
+ */
+ @MessageMapping("/chat/{roomId}/message")
+ @SendTo("/sub/chat/{roomId}/messages")
+ public ResponseEntity receiveMessage(
+ @DestinationVariable final Long roomId,
+ @RequestBody final ChatMessageRequest chatMessageRequest
+ ) {
+ // 메시지를 해당 채팅방 구독자들에게 전송
+ ChatMessageResponse chatMessageResponse = ChatMessageResponse.builder()
+ .nickname(chatMessageRequest.getNickname())
+ .message(chatMessageRequest.getMessage())
+ .roomId(roomId)
+ .date(chatMessageRequest.getDate())
+ .build();
+ return ResponseEntity.ok(chatMessageResponse);
+ }
+
+ @GetMapping("/chat/rooms")
+ public ResponseEntity getChatRooms() {
+ return ResponseEntity.ok(chatService.getChatRooms());
+ }
+}
diff --git a/src/main/java/com/memetitle/chat/repository/ChatRoomRepository.java b/src/main/java/com/memetitle/chat/repository/ChatRoomRepository.java
new file mode 100644
index 0000000..6a575ac
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/repository/ChatRoomRepository.java
@@ -0,0 +1,17 @@
+package com.memetitle.chat.repository;
+
+import com.memetitle.chat.domain.ChatRoom;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+public interface ChatRoomRepository extends JpaRepository {
+
+ @Modifying
+ @Query(value = "UPDATE ChatRoom cr set cr.memberCount = cr.memberCount + 1 where cr.id = :id")
+ void increaseMemberCount(Long id);
+
+ @Modifying
+ @Query(value = "UPDATE ChatRoom cr set cr.memberCount = cr.memberCount - 1 where cr.id = :id")
+ void decreaseMemberCount(Long id);
+}
diff --git a/src/main/java/com/memetitle/chat/repository/ChatRoomStorage.java b/src/main/java/com/memetitle/chat/repository/ChatRoomStorage.java
new file mode 100644
index 0000000..f2b2437
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/repository/ChatRoomStorage.java
@@ -0,0 +1,26 @@
+package com.memetitle.chat.repository;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Repository;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+@Repository
+@Slf4j
+public class ChatRoomStorage {
+
+ private final ConcurrentMap chatRoomMembers = new ConcurrentHashMap<>();
+
+ public void storage(String sessionId, Long roomId) {
+ chatRoomMembers.put(sessionId, roomId);
+ }
+
+ public Long getRoomId(String sessionId) {
+ return chatRoomMembers.get(sessionId);
+ }
+
+ public void delete(String sessionId) {
+ chatRoomMembers.remove(sessionId);
+ }
+}
diff --git a/src/main/java/com/memetitle/chat/service/ChatService.java b/src/main/java/com/memetitle/chat/service/ChatService.java
new file mode 100644
index 0000000..e7715dd
--- /dev/null
+++ b/src/main/java/com/memetitle/chat/service/ChatService.java
@@ -0,0 +1,32 @@
+package com.memetitle.chat.service;
+
+import com.memetitle.chat.domain.ChatRoom;
+import com.memetitle.chat.dto.response.ChatRoomsResponse;
+import com.memetitle.chat.repository.ChatRoomRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ChatService {
+
+ private final ChatRoomRepository chatRoomRepository;
+
+ public ChatRoomsResponse getChatRooms() {
+
+ final List chatRooms = chatRoomRepository.findAll();
+ return ChatRoomsResponse.ofChatRooms(chatRooms);
+ }
+
+ public void increaseMemberCount(final Long roomId) {
+ chatRoomRepository.increaseMemberCount(roomId);
+ }
+
+ public void decreaseMemberCount(final Long roomId) {
+ chatRoomRepository.decreaseMemberCount(roomId);
+ }
+}
diff --git a/src/main/java/com/memetitle/comment/domain/Comment.java b/src/main/java/com/memetitle/comment/domain/Comment.java
index 88beb50..c198a0f 100644
--- a/src/main/java/com/memetitle/comment/domain/Comment.java
+++ b/src/main/java/com/memetitle/comment/domain/Comment.java
@@ -11,7 +11,7 @@
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
-import javax.persistence.*;
+import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
diff --git a/src/main/java/com/memetitle/comment/domain/CommentLike.java b/src/main/java/com/memetitle/comment/domain/CommentLike.java
index dbe36c8..36a35a6 100644
--- a/src/main/java/com/memetitle/comment/domain/CommentLike.java
+++ b/src/main/java/com/memetitle/comment/domain/CommentLike.java
@@ -7,7 +7,7 @@
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
-import javax.persistence.*;
+import jakarta.persistence.*;
@Entity
@Getter
diff --git a/src/main/java/com/memetitle/comment/dto/request/CommentCreateRequest.java b/src/main/java/com/memetitle/comment/dto/request/CommentCreateRequest.java
index 0abfd73..cbcf280 100644
--- a/src/main/java/com/memetitle/comment/dto/request/CommentCreateRequest.java
+++ b/src/main/java/com/memetitle/comment/dto/request/CommentCreateRequest.java
@@ -3,7 +3,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
-import javax.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotBlank;
@NoArgsConstructor()
@Getter
diff --git a/src/main/java/com/memetitle/comment/dto/request/CommentModifyRequest.java b/src/main/java/com/memetitle/comment/dto/request/CommentModifyRequest.java
index c8fcfc2..8bafc22 100644
--- a/src/main/java/com/memetitle/comment/dto/request/CommentModifyRequest.java
+++ b/src/main/java/com/memetitle/comment/dto/request/CommentModifyRequest.java
@@ -3,7 +3,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
-import javax.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotBlank;
@NoArgsConstructor()
@Getter
diff --git a/src/main/java/com/memetitle/comment/dto/response/CommentsResponse.java b/src/main/java/com/memetitle/comment/dto/response/CommentsResponse.java
index c8eaac8..f4b6dc4 100644
--- a/src/main/java/com/memetitle/comment/dto/response/CommentsResponse.java
+++ b/src/main/java/com/memetitle/comment/dto/response/CommentsResponse.java
@@ -22,7 +22,7 @@ public class CommentsResponse {
public static CommentsResponse ofComments(Page comments) {
final List commentElement = comments.stream()
.map(CommentElement::of)
- .collect(Collectors.toList());
+ .toList();
return CommentsResponse.builder()
.comments(commentElement)
@@ -36,7 +36,7 @@ public static CommentsResponse ofComments(Page comments) {
public static CommentsResponse ofCommentDtos(Page comments) {
final List commentElement = comments.stream()
.map(CommentElement::of)
- .collect(Collectors.toList());
+ .toList();
return CommentsResponse.builder()
.comments(commentElement)
diff --git a/src/main/java/com/memetitle/comment/presentation/CommentController.java b/src/main/java/com/memetitle/comment/presentation/CommentController.java
index b9fedee..5e5a7b3 100644
--- a/src/main/java/com/memetitle/comment/presentation/CommentController.java
+++ b/src/main/java/com/memetitle/comment/presentation/CommentController.java
@@ -12,7 +12,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
-import javax.validation.Valid;
+import jakarta.validation.Valid;
import java.net.URI;
import static org.springframework.data.domain.Sort.Direction.DESC;
diff --git a/src/main/java/com/memetitle/comment/repository/CommentCustomRepository.java b/src/main/java/com/memetitle/comment/repository/CommentCustomRepository.java
new file mode 100644
index 0000000..bf892f4
--- /dev/null
+++ b/src/main/java/com/memetitle/comment/repository/CommentCustomRepository.java
@@ -0,0 +1,13 @@
+package com.memetitle.comment.repository;
+
+import com.memetitle.comment.dto.CommentDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface CommentCustomRepository {
+
+ Page findByTitleId(Long titleId, Pageable pageable);
+
+ Page findByTitleId(Long memberId, Long titleId, Pageable pageable);
+
+}
diff --git a/src/main/java/com/memetitle/comment/repository/CommentCustomRepositoryImpl.java b/src/main/java/com/memetitle/comment/repository/CommentCustomRepositoryImpl.java
new file mode 100644
index 0000000..e5a62d4
--- /dev/null
+++ b/src/main/java/com/memetitle/comment/repository/CommentCustomRepositoryImpl.java
@@ -0,0 +1,119 @@
+package com.memetitle.comment.repository;
+
+import com.memetitle.comment.domain.Comment;
+import com.memetitle.comment.domain.QComment;
+import com.memetitle.comment.domain.QCommentLike;
+import com.memetitle.comment.dto.CommentDto;
+import com.memetitle.member.domain.QMember;
+import com.querydsl.core.types.Order;
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.CaseBuilder;
+import com.querydsl.core.types.dsl.Expressions;
+import com.querydsl.core.types.dsl.PathBuilder;
+import com.querydsl.jpa.impl.JPAQuery;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Repository
+public class CommentCustomRepositoryImpl implements CommentCustomRepository{
+
+ private final JPAQueryFactory jpaQueryFactory;
+
+ @Override
+ public Page findByTitleId(Long titleId, Pageable pageable) {
+ QComment comment = QComment.comment;
+ QMember member = QMember.member;
+
+ // Main query
+ List content = jpaQueryFactory
+ .select(Projections.constructor(CommentDto.class,
+ comment.id,
+ comment.title.id,
+ comment.title.memeId,
+ comment.content,
+ comment.member.id,
+ comment.member.nickname,
+ comment.member.imgUrl,
+ comment.likeCount,
+ comment.createdAt,
+ Expressions.constant(false),
+ Expressions.constant(false)
+ ))
+ .from(comment)
+ .leftJoin(comment.member, member)
+ .where(comment.title.id.eq(titleId))
+ .offset(pageable.getOffset())
+ .orderBy(getOrderSpecifier(pageable.getSort()).stream().toArray(OrderSpecifier[]::new))
+ .limit(pageable.getPageSize())
+ .fetch();
+
+ // Count query
+ JPAQuery countQuery = jpaQueryFactory
+ .select(comment.count())
+ .from(comment)
+ .where(comment.title.id.eq(titleId));
+
+ return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
+ }
+
+ @Override
+ public Page findByTitleId(Long memberId, Long titleId, Pageable pageable) {
+ QComment comment = QComment.comment;
+ QCommentLike commentLike = QCommentLike.commentLike;
+ QMember member = QMember.member;
+
+ List content = jpaQueryFactory
+ .select(Projections.constructor(CommentDto.class,
+ comment.id,
+ comment.title.id,
+ comment.title.memeId,
+ comment.content,
+ comment.member.id,
+ comment.member.nickname,
+ comment.member.imgUrl,
+ comment.likeCount,
+ comment.createdAt,
+ new CaseBuilder()
+ .when(commentLike.id.isNull()).then(false)
+ .otherwise(true).as("isLiked"),
+ new CaseBuilder()
+ .when(comment.member.id.eq(memberId)).then(true)
+ .otherwise(false).as("isOwner")
+ ))
+ .from(comment)
+ .leftJoin(comment.member, member)
+ .leftJoin(commentLike).on(comment.id.eq(commentLike.comment.id)
+ .and(commentLike.member.id.eq(memberId)))
+ .where(comment.title.id.eq(titleId))
+ .offset(pageable.getOffset())
+ .orderBy(getOrderSpecifier(pageable.getSort()).stream().toArray(OrderSpecifier[]::new))
+ .limit(pageable.getPageSize())
+ .fetch();
+
+ JPAQuery countQuery = jpaQueryFactory
+ .select(comment.count())
+ .from(comment)
+ .where(comment.title.id.eq(titleId));
+
+ return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
+ }
+
+ private List getOrderSpecifier(Sort sort) {
+ return sort.stream()
+ .map(order -> {
+ Order direction = order.isAscending() ? Order.ASC : Order.DESC;
+ PathBuilder orderByExpression = new PathBuilder(Comment.class, "comment");
+ return new OrderSpecifier(direction, orderByExpression.get(order.getProperty()));
+ })
+ .toList();
+ }
+}
diff --git a/src/main/java/com/memetitle/comment/repository/CommentRepository.java b/src/main/java/com/memetitle/comment/repository/CommentRepository.java
index 0523759..62bf489 100644
--- a/src/main/java/com/memetitle/comment/repository/CommentRepository.java
+++ b/src/main/java/com/memetitle/comment/repository/CommentRepository.java
@@ -9,19 +9,19 @@
import org.springframework.data.jpa.repository.Query;
-public interface CommentRepository extends JpaRepository {
+public interface CommentRepository extends JpaRepository, CommentCustomRepository{
- @Query(value = "SELECT new com.memetitle.comment.dto.CommentDto(c.id, c.title.id, c.title.memeId, c.content, c.member.id, c.member.nickname, c.member.imgUrl, c.likeCount, c.createdAt, false, false)" +
- " from Comment c LEFT JOIN c.member m " +
- " WHERE c.title.id = :titleId"
- ,countQuery = "select count(c) from Comment c where c.title.id = :titleId")
- Page findByTitleId(Long titleId, Pageable pageable);
+// @Query(value = "SELECT new com.memetitle.comment.dto.CommentDto(c.id, c.title.id, c.title.memeId, c.content, c.member.id, c.member.nickname, c.member.imgUrl, c.likeCount, c.createdAt, false, false)" +
+// " from Comment c LEFT JOIN c.member m " +
+// " WHERE c.title.id = :titleId"
+// ,countQuery = "select count(c) from Comment c where c.title.id = :titleId")
+// Page findByTitleId(Long titleId, Pageable pageable);
- @Query(value = "SELECT new com.memetitle.comment.dto.CommentDto(c.id, c.title.id, c.title.memeId, c.content, c.member.id, c.member.nickname, c.member.imgUrl, c.likeCount, c.createdAt, (CASE WHEN cl.id IS NULL THEN false ELSE true END), (CASE WHEN c.member.id = :memberId THEN true ELSE false END))" +
- " from Comment c LEFT JOIN c.member m " +
- " LEFT JOIN CommentLike cl ON c.id = cl.comment.id AND cl.member.id = :memberId" +
- " WHERE c.title.id = :titleId")
- Page findByTitleId(Long memberId, Long titleId, Pageable pageable);
+// @Query(value = "SELECT new com.memetitle.comment.dto.CommentDto(c.id, c.title.id, c.title.memeId, c.content, c.member.id, c.member.nickname, c.member.imgUrl, c.likeCount, c.createdAt, (CASE WHEN cl.id IS NULL THEN false ELSE true END), (CASE WHEN c.member.id = :memberId THEN true ELSE false END))" +
+// " from Comment c LEFT JOIN c.member m " +
+// " LEFT JOIN CommentLike cl ON c.id = cl.comment.id AND cl.member.id = :memberId" +
+// " WHERE c.title.id = :titleId")
+// Page findByTitleId(Long memberId, Long titleId, Pageable pageable);
@EntityGraph(attributePaths = {"member"})
Page findByMemberId(Long memberId, Pageable pageable);
diff --git a/src/main/java/com/memetitle/global/aspect/LogAspect.java b/src/main/java/com/memetitle/global/aspect/LogAspect.java
index c272e82..0554dca 100644
--- a/src/main/java/com/memetitle/global/aspect/LogAspect.java
+++ b/src/main/java/com/memetitle/global/aspect/LogAspect.java
@@ -2,13 +2,13 @@
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
-import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
@Aspect
@Slf4j
@@ -21,14 +21,16 @@ public void controller() {
@AfterReturning(pointcut = "controller()", returning = "responseEntity")
public void afterReturning(ResponseEntity> responseEntity) {
- HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
- if (request != null) {
- HttpStatus statusCode = responseEntity.getStatusCode();
- if (statusCode != null) {
- log.info("[Response sent: {} {} {}]", request.getMethod(), request.getRequestURI(), statusCode);
- } else {
- log.info("[Response sent: {} {}]", request.getMethod(), request.getRequestURI());
+ try {
+ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
+ if (request != null) {
+ HttpStatusCode statusCode = responseEntity.getStatusCode();
+ if (statusCode != null) {
+ log.info("[Response sent: {} {} {}]", request.getMethod(), request.getRequestURI(), statusCode);
+ } else {
+ log.info("[Response sent: {} {}]", request.getMethod(), request.getRequestURI());
+ }
}
- }
+ } catch (IllegalStateException e) {}
}
}
diff --git a/src/main/java/com/memetitle/global/common/interceptor/PathMatcherInterceptor.java b/src/main/java/com/memetitle/global/common/interceptor/PathMatcherInterceptor.java
index 8129be6..9645f49 100644
--- a/src/main/java/com/memetitle/global/common/interceptor/PathMatcherInterceptor.java
+++ b/src/main/java/com/memetitle/global/common/interceptor/PathMatcherInterceptor.java
@@ -2,8 +2,8 @@
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;
public class PathMatcherInterceptor implements HandlerInterceptor {
private final HandlerInterceptor handlerInterceptor;
diff --git a/src/main/java/com/memetitle/global/config/QueryDSLConfig.java b/src/main/java/com/memetitle/global/config/QueryDSLConfig.java
new file mode 100644
index 0000000..92a5761
--- /dev/null
+++ b/src/main/java/com/memetitle/global/config/QueryDSLConfig.java
@@ -0,0 +1,18 @@
+package com.memetitle.global.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@RequiredArgsConstructor
+public class QueryDSLConfig {
+ private final EntityManager entityManager;
+
+ @Bean
+ public JPAQueryFactory jpaQueryFactory(){
+ return new JPAQueryFactory(entityManager);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/memetitle/global/exception/ErrorCode.java b/src/main/java/com/memetitle/global/exception/ErrorCode.java
index 89c57af..3a55c75 100644
--- a/src/main/java/com/memetitle/global/exception/ErrorCode.java
+++ b/src/main/java/com/memetitle/global/exception/ErrorCode.java
@@ -12,6 +12,7 @@ public enum ErrorCode {
NOT_FOUND_MEME_ID(2001, "해당 밈을 찾을 수 없습니다."),
NOT_FOUND_TITLE_ID(2002, "해당 제목을 찾을 수 없습니다."),
NOT_FOUND_COMMENT_ID(2003, "해당 댓글을 찾을 수 없습니다."),
+ NOT_FOUND_CHATROOM_ID(2009, "해당 채팅방을 찾을 수 없습니다."),
NOT_FOUND_TITLE_LIKE(2004, "해당 제목에 좋아요가 없습니다."),
NOT_FOUND_COMMENT_LIKE(2005, "해당 댓글에 좋아요가 없습니다."),
TITLE_ACCESS_DENIED(2012, "해당 제목에 대한 권한이 없습니다."),
diff --git a/src/main/java/com/memetitle/global/exception/GlobalExceptionHandler.java b/src/main/java/com/memetitle/global/exception/GlobalExceptionHandler.java
index c905c0d..16fdd91 100644
--- a/src/main/java/com/memetitle/global/exception/GlobalExceptionHandler.java
+++ b/src/main/java/com/memetitle/global/exception/GlobalExceptionHandler.java
@@ -2,7 +2,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -27,7 +27,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
protected ResponseEntity