Skip to content
Open
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
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ dependencies {

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

// WebSocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// MongoDB
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

// JSON 파싱
implementation 'com.fasterxml.jackson.core:jackson-databind'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package study.spring_boot_c.domain.chat.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import study.spring_boot_c.domain.chat.application.ChatService;
import study.spring_boot_c.domain.chat.dto.ChatMessageDTO;
import study.spring_boot_c.global.common.response.BaseResponse;
import study.spring_boot_c.global.error.code.status.SuccessStatus;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/chat")
@Validated
public class ChatController {

private final ChatService chatService;

@PostMapping("/send")
@Operation(summary = "채팅 메시지 전송 API", description = "클라이언트가 채팅 메시지를 전송할 때 사용하는 API입니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CHAT_200", description = "메시지 전송 성공")
})
@Parameters({
@Parameter(name = "roomId", description = "채팅방 ID"),
@Parameter(name = "senderId", description = "메시지를 보낸 사용자 ID"),
@Parameter(name = "message", description = "보낼 메시지 내용")
})
public BaseResponse<Void> sendChatMessage(@Valid @RequestBody ChatMessageDTO.MessageReceive request) {

chatService.sendMessage(request);

return BaseResponse.onSuccess(SuccessStatus.CHAT_SEND_SUCCESS, null);
}

@GetMapping("/room/{roomId}/message")
@Operation(summary = "채팅방 메시지 조회 API", description = "클라이언트가 채팅 메시지를 전송할 때 사용하는 API입니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "CHAT_200", description = "메시지 조회 성공")
})
public BaseResponse<Page<ChatMessageDTO.RoomMessage>> getMessagesByRoomId(@Valid @PathVariable Long roomId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {

Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").ascending());
Page<ChatMessageDTO.RoomMessage> result = chatService.getMessagesByRoomId(roomId, pageable);

return BaseResponse.onSuccess(SuccessStatus.CHAT_SEND_SUCCESS, result);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package study.spring_boot_c.domain.chat.api;

import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import study.spring_boot_c.domain.chat.application.ChatService;
import study.spring_boot_c.domain.chat.dto.ChatMessageDTO;

@Controller
@RequiredArgsConstructor
public class ChatWebSocketController {

private final ChatService chatService;

@MessageMapping("/chat/send")
public void handleWebSocketMessage(ChatMessageDTO.MessageReceive message) {
chatService.sendMessage(message);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package study.spring_boot_c.domain.chat.application;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import study.spring_boot_c.domain.chat.dto.ChatMessageDTO;

public interface ChatService {
void sendMessage(ChatMessageDTO.MessageReceive dto);

Page<ChatMessageDTO.RoomMessage> getMessagesByRoomId(Long roomId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package study.spring_boot_c.domain.chat.application;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.spring_boot_c.domain.chat.converter.ChatMessageConverter;
import study.spring_boot_c.domain.chat.domain.entity.ChatMessage;
import study.spring_boot_c.domain.chat.domain.repository.ChatMessageRepository;
import study.spring_boot_c.domain.chat.dto.ChatMessageDTO;
import study.spring_boot_c.domain.chat.exception.ChatException;
import study.spring_boot_c.global.error.code.status.ErrorStatus;
import study.spring_boot_c.global.redis.RedisPublisher;

@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService{
private final RedisPublisher redisPublisher;
private final ChatMessageRepository chatMessageRepository; // MongoDB 저장용
private final ObjectMapper objectMapper;

@Transactional
@Override
public void sendMessage(ChatMessageDTO.MessageReceive dto) {
// 멤버 체크 + 방 체크 구현 로직 필요
ChatMessage message = ChatMessageConverter.toChatMessage(dto);
String channel = "chatroom:" + dto.getRoomId();

try {
chatMessageRepository.save(message);
} catch (Exception e) {
throw new ChatException(ErrorStatus.DB_ERROR);
}

try {
String json = objectMapper.writeValueAsString(message);
redisPublisher.publish(channel, json);
} catch (Exception e) {
throw new ChatException(ErrorStatus.REDIS_ERROR);
}

}

@Override
public Page<ChatMessageDTO.RoomMessage> getMessagesByRoomId(Long roomId, Pageable pageable) {
return chatMessageRepository.findByRoomIdOrderByTimestampAsc(roomId, pageable)
.map(ChatMessageConverter::toRoomMessages);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package study.spring_boot_c.domain.chat.converter;

import org.springframework.stereotype.Component;
import study.spring_boot_c.domain.chat.domain.entity.ChatMessage;
import study.spring_boot_c.domain.chat.dto.ChatMessageDTO;

import java.time.LocalDateTime;

@Component
public class ChatMessageConverter {

public static ChatMessage toChatMessage(ChatMessageDTO.MessageReceive dto) {
return ChatMessage.builder()
.roomId(dto.getRoomId())
.senderId(dto.getSenderId())
.message(dto.getMessage())
.timestamp(LocalDateTime.now())
.build();
}

public static ChatMessageDTO.RoomMessage toRoomMessages(ChatMessage entity) {
return ChatMessageDTO.RoomMessage.builder()
.senderId(entity.getSenderId())
.message(entity.getMessage())
.timestamp(entity.getTimestamp())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package study.spring_boot_c.domain.chat.domain.entity;

import lombok.Builder;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import study.spring_boot_c.domain.model.entity.BaseEntity;

import java.time.LocalDateTime;

@Builder
@Getter
@Document(collection = "chat_messages")
public class ChatMessage {
@Id
private String id;
private Long roomId;
private Long senderId;
private String message;
private LocalDateTime timestamp;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package study.spring_boot_c.domain.chat.domain.entity;

import lombok.*;
import jakarta.persistence.*;
import study.spring_boot_c.domain.model.entity.BaseEntity;

import java.time.LocalDateTime;
import java.util.List;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ChatNotification extends BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long memberId;
private String previewMessage;
private boolean isRead;
private LocalDateTime notifiedAt;

@ManyToOne
@JoinColumn(name = "chat_room_id")
private ChatRoom chatRoom;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package study.spring_boot_c.domain.chat.domain.entity;

import jakarta.persistence.*;
import lombok.*;
import study.spring_boot_c.domain.model.entity.BaseEntity;

import java.time.LocalDateTime;
import java.util.List;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ChatRoom extends BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private LocalDateTime createdAt;

@OneToMany(mappedBy = "chatRoom")
private List<ChatNotification> notifications;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package study.spring_boot_c.domain.chat.domain.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
import study.spring_boot_c.domain.chat.domain.entity.ChatMessage;

public interface ChatMessageRepository extends MongoRepository<ChatMessage, String> {
Page<ChatMessage> findByRoomIdOrderByTimestampAsc(Long roomId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package study.spring_boot_c.domain.chat.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

public class ChatMessageDTO {

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class MessageReceive {
private Long roomId;
private Long senderId;
private String message;
private LocalDateTime timestamp;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class RoomMessage {
private Long senderId;
private String message;
private LocalDateTime timestamp;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package study.spring_boot_c.domain.chat.exception;

import study.spring_boot_c.global.error.code.BaseErrorCode;
import study.spring_boot_c.global.error.exception.GeneralException;

public class ChatException extends GeneralException {

public ChatException(BaseErrorCode code) {
super(code);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public class Manner extends BaseEntity {
private int score;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "evaluatee_id", nullable = false)
private Member evaluatee;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "evaluator_id", nullable = false)
private Member evaluator;

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public class SaleReview extends BaseEntity {
private String content;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "reviewer_id", nullable = false)
private Member reviewer;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "reviewee_id", nullable = false)
private Member reviewee;

@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public class CarrotNotification extends BaseEntity {
private int score;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "evaluatee_id", nullable = false)
private Member evaluatee;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "evaluator_id", nullable = false)
private Member evaluator;

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package study.spring_boot_c.domain.notification.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import study.spring_boot_c.domain.notification.domain.entity.CarrotNotification;

public interface CarrotNotificationRepository extends JpaRepository<CarrotNotificationRepository,Long> {
public interface CarrotNotificationRepository extends JpaRepository<CarrotNotification,Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ public class Product extends BaseEntity {
private Category category;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "seller_id", nullable = false)
private Member seller;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@JoinColumn(name = "buyer_id", nullable = false)
private Member buyer;

}
Loading