diff --git a/src/backend/history_server/build.gradle b/src/backend/history_server/build.gradle index 289d0726..5e74347c 100644 --- a/src/backend/history_server/build.gradle +++ b/src/backend/history_server/build.gradle @@ -22,12 +22,19 @@ repositories { } dependencies { + //common-module + implementation project(":common_module") + //spring boot implementation 'org.springframework.boot:spring-boot-starter-web' - //mongo db + //db - mongo implementation('org.springframework.boot:spring-boot-starter-data-mongodb') + //db - postgresql + implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + //test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/backend/history_server/settings.gradle b/src/backend/history_server/settings.gradle index f667b5a3..71841687 100644 --- a/src/backend/history_server/settings.gradle +++ b/src/backend/history_server/settings.gradle @@ -1 +1,4 @@ rootProject.name = 'history_server' + +include(":common_module") +findProject(":common_module")?.projectDir = file("../common_module") diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/HistoryServerApplication.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/HistoryServerApplication.java index 691913ab..d90d0747 100644 --- a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/HistoryServerApplication.java +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/HistoryServerApplication.java @@ -5,7 +5,11 @@ import org.springframework.data.mongodb.config.EnableMongoAuditing; @EnableMongoAuditing -@SpringBootApplication +@SpringBootApplication(scanBasePackages = { + "com.jootalkpia.history_server", + "com.jootalkpia.passport", + "com.jootalkpia.config", +}) public class HistoryServerApplication { public static void main(String[] args) { diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/controller/HistoryController.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/controller/HistoryController.java new file mode 100644 index 00000000..f8215512 --- /dev/null +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/controller/HistoryController.java @@ -0,0 +1,31 @@ +package com.jootalkpia.history_server.controller; + +import com.jootalkpia.history_server.dto.ChatMessagePageResponse; +import com.jootalkpia.history_server.service.HistoryQueryService; +import com.jootalkpia.passport.anotation.CurrentUser; +import com.jootalkpia.passport.component.UserInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class HistoryController { + + private static final String DEFAULT_PAGE_SIZE = "30"; + private final HistoryQueryService historyQueryService; + + @GetMapping("/api/v1/history/{channelId}") + public ChatMessagePageResponse getChatMessagesForward( + @PathVariable Long channelId, + //처음 요청시엔 서버 내에서 안읽은 메세지 값으로 설정하기 위해 false + // 이때 message의 objectId가 아닌 threadId가 기준 + @RequestParam(required = false) Long cursorId, + @RequestParam(defaultValue = DEFAULT_PAGE_SIZE) int size, + @CurrentUser UserInfo userInfo) { + return historyQueryService. + getChatMessagesForward(channelId, cursorId, size, userInfo.userId()); + } +} diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/BaseTimeEntity.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/BaseTimeEntity.java index 9cdcafcf..786bdb12 100644 --- a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/BaseTimeEntity.java +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/BaseTimeEntity.java @@ -1,5 +1,6 @@ package com.jootalkpia.history_server.domain; +import jakarta.persistence.Column; import java.time.LocalDateTime; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; @@ -9,6 +10,7 @@ public abstract class BaseTimeEntity { @CreatedDate + @Column(updatable = false) private LocalDateTime createdAt; // 생성 시간 @LastModifiedDate diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/UserChannel.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/UserChannel.java new file mode 100644 index 00000000..e971c175 --- /dev/null +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/UserChannel.java @@ -0,0 +1,26 @@ +package com.jootalkpia.history_server.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Entity +@Table(name = "user_channel") +@Getter +@RequiredArgsConstructor +public class UserChannel extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long userChannelId; + + private Long userId; + + private Long lastReadId; + + private Long channelId; +} \ No newline at end of file diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/ChatMessagePageResponse.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/ChatMessagePageResponse.java new file mode 100644 index 00000000..95c53648 --- /dev/null +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/ChatMessagePageResponse.java @@ -0,0 +1,10 @@ +package com.jootalkpia.history_server.dto; + +import java.util.List; + +public record ChatMessagePageResponse( + boolean hasNext, + Long lastCursorId, + List threads +) {} + diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/MessageDto.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/MessageDto.java index 4e3a8045..f19cafeb 100644 --- a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/MessageDto.java +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/MessageDto.java @@ -1,7 +1,9 @@ package com.jootalkpia.history_server.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import com.jootalkpia.history_server.domain.Message; +@JsonInclude(JsonInclude.Include.NON_NULL) public record MessageDto( String type, String text, @@ -24,5 +26,17 @@ public Message toMessage() { .videoUrl(this.videoUrl) .build(); } + public static MessageDto documentToDto(Message message) { + return new MessageDto( + message.getType(), + message.getText(), + message.getImageId(), + message.getImageUrl(), + message.getVideoId(), + message.getVideoThumbnailId(), + message.getThumbnailUrl(), + message.getVideoUrl() + ); + } } diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/ThreadDto.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/ThreadDto.java new file mode 100644 index 00000000..a4f034f5 --- /dev/null +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/dto/ThreadDto.java @@ -0,0 +1,30 @@ +package com.jootalkpia.history_server.dto; + +import com.jootalkpia.history_server.domain.ChatMessage; +import java.util.List; +import java.util.stream.Collectors; + +public record ThreadDto( + Long channelId, + Long threadId, + String threadDateTime, + Long userId, + String userNickname, + String userProfileImage, + List messages +) { + public static ThreadDto from(ChatMessage chatMessage) { + return new ThreadDto( + chatMessage.getChannelId(), + chatMessage.getThreadId(), + chatMessage.getThreadDateTime(), + chatMessage.getUserId(), + chatMessage.getUserNickname(), + chatMessage.getUserProfileImage(), + chatMessage.getMessages().stream() + .map(MessageDto::documentToDto) // Message 변환 + .collect(Collectors.toList()) + ); + } +} + diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/repository/ChatMessageRepository.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/repository/ChatMessageRepository.java index bcac778b..306ffcbb 100644 --- a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/repository/ChatMessageRepository.java +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/repository/ChatMessageRepository.java @@ -1,7 +1,14 @@ package com.jootalkpia.history_server.repository; import com.jootalkpia.history_server.domain.ChatMessage; +import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; -public interface ChatMessageRepository extends MongoRepository { +import java.util.List; + +@Repository +public interface ChatMessageRepository extends MongoRepository { + // cursorId 이후의 메시지 조회 (페이징) + List findByChannelIdAndThreadIdGreaterThanOrderByThreadIdAsc(Long channelId, Long threadId, Pageable pageable); } diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/repository/UserChannelRepository.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/repository/UserChannelRepository.java new file mode 100644 index 00000000..01defa3a --- /dev/null +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/repository/UserChannelRepository.java @@ -0,0 +1,14 @@ +package com.jootalkpia.history_server.repository; + +import com.jootalkpia.history_server.domain.UserChannel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserChannelRepository extends JpaRepository { + + @Query("SELECT u.lastReadId FROM UserChannel u WHERE u.userId = :userId AND u.channelId = :channelId") + Long findLastReadIdByUserIdAndChannelId(@Param("userId") Long userId, @Param("channelId") Long channelId); +} diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/service/HistoryQueryService.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/service/HistoryQueryService.java new file mode 100644 index 00000000..d5f10734 --- /dev/null +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/service/HistoryQueryService.java @@ -0,0 +1,86 @@ +package com.jootalkpia.history_server.service; + +import com.jootalkpia.history_server.domain.ChatMessage; +import com.jootalkpia.history_server.dto.ThreadDto; +import com.jootalkpia.history_server.dto.ChatMessagePageResponse; +import com.jootalkpia.history_server.repository.ChatMessageRepository; +import com.jootalkpia.history_server.repository.UserChannelRepository; +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class HistoryQueryService { + + private final ChatMessageRepository chatMessageRepository; + private final UserChannelRepository userChannelRepository; + + public ChatMessagePageResponse getChatMessagesForward(Long channelId, Long cursorId, int size, Long userId) { + List chatMessageList = fetchMessagesForward(channelId, cursorId, size, userId); + + if (chatMessageList.isEmpty()) { + return new ChatMessagePageResponse(false, null, Collections.emptyList()); + } + + List responseMessages = convertToDtoList(chatMessageList); + boolean hasNext = determineHasNext(responseMessages, size); + + // size + 1로 조회했으므로, 초과한 1개 데이터 제거 + if (hasNext) { + responseMessages = responseMessages.subList(0, size); + } + + Long lastThreadId = getLastThreadId(responseMessages); + + return new ChatMessagePageResponse(hasNext, lastThreadId, responseMessages); + } + + + /** + * DB에서 채팅 메시지를 조회하는 메서드 + */ + private List fetchMessagesForward(Long channelId, Long cursorId, int size, Long userId) { + if (cursorId == null) { + Long lastReadId = userChannelRepository.findLastReadIdByUserIdAndChannelId(userId, channelId); + + if (lastReadId == null) { + return Collections.emptyList(); // 첫 입장 시 빈 응답 + } + + return chatMessageRepository.findByChannelIdAndThreadIdGreaterThanOrderByThreadIdAsc( + channelId, lastReadId, PageRequest.of(0, size + 1)); + } + + return chatMessageRepository.findByChannelIdAndThreadIdGreaterThanOrderByThreadIdAsc( + channelId, cursorId, PageRequest.of(0, size + 1)); + } + + /** + * ChatMessage 리스트를 ChatMessageDto 리스트로 변환하는 메서드 + */ + private List convertToDtoList(List chatMessageList) { + return chatMessageList.stream() + .map(ThreadDto::from) + .toList(); + } + + /** + * hasNext(다음 페이지 여부)를 판별하는 메서드 + */ + private boolean determineHasNext(List responseMessages, int size) { + return responseMessages.size() > size; + } + + /** + * 마지막 threadId를 반환하는 메서드 + */ + private Long getLastThreadId(List responseMessages) { + if (responseMessages.isEmpty()) { + return null; + } + return responseMessages.get(responseMessages.size() - 1).threadId(); + } +} diff --git a/src/backend/history_server/src/main/resources/application.yml b/src/backend/history_server/src/main/resources/application.yml index 2675ae00..34b0a747 100644 --- a/src/backend/history_server/src/main/resources/application.yml +++ b/src/backend/history_server/src/main/resources/application.yml @@ -1,4 +1,19 @@ spring: + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME} + username: ${DB_USER} + password: ${DB_PASSWORD} + + jpa: + show-sql: true + hibernate: + ddl-auto: none + properties: + hibernate: + format_sql: true + show_sql: true + data: mongodb: uri: ${MONGO_URI}