diff --git a/src/backend/chat-server/build.gradle b/src/backend/chat-server/build.gradle
index 9d66e9fa..8fe84697 100644
--- a/src/backend/chat-server/build.gradle
+++ b/src/backend/chat-server/build.gradle
@@ -18,6 +18,7 @@ repositories {
}
dependencies {
+ implementation 'io.github.dnovitski:logback-awslogs-appender:1.7.2'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.kafka:spring-kafka'
diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/WebSocketController.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/WebSocketController.java
index 9b389d4e..98222590 100644
--- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/WebSocketController.java
+++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/controller/WebSocketController.java
@@ -1,5 +1,8 @@
package kickzo.stomp_chat.controller;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
import com.fasterxml.jackson.databind.ObjectMapper;
import kickzo.stomp_chat.enums.EventType;
@@ -75,5 +78,12 @@ public void playTime(String payload) throws Exception {
public record UserConnectRequest(long userId) {}
public record SendMessageRequest(long roomId, long userId, String nickname, int role, String profileImageUrl, String content, String message) {}
- public record PlayTimeRequest(long roomId, long playTime, String playerState) {}
+ public record PlayTimeRequest(long roomId, BigDecimal playTime, String playerState) {
+ public PlayTimeRequest(long roomId, BigDecimal playTime, String playerState) {
+ this.roomId = roomId;
+ this.playTime = playTime.setScale(2, RoundingMode.HALF_UP); // 소수점 2자리로 변환
+ this.playerState = playerState;
+ }
+ }
+
}
diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/playlist/PlayTime.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/playlist/PlayTime.java
index 4d40c5be..31861c9d 100644
--- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/playlist/PlayTime.java
+++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/playlist/PlayTime.java
@@ -1,5 +1,8 @@
package kickzo.stomp_chat.dto.playlist;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -9,6 +12,6 @@
@NoArgsConstructor
public class PlayTime {
private long roomId;
- private long playTime;
+ private BigDecimal playTime;
private String playerState;
}
diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/room/ChatMessage.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/room/ChatMessage.java
index 40d85557..f72368b3 100644
--- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/room/ChatMessage.java
+++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/dto/room/ChatMessage.java
@@ -1,3 +1,3 @@
package kickzo.stomp_chat.dto.room;
-public record ChatMessage(long roomId, long userId, String nickname, int role, String profileImageUrl, String content, String message) {}
+public record ChatMessage(long roomId, long userId, String nickname, int role, String profileImageUrl, String content, String message, long timestamp) {}
diff --git a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/WebSocketRoomService.java b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/WebSocketRoomService.java
index 2354b7a8..e0f00b2f 100644
--- a/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/WebSocketRoomService.java
+++ b/src/backend/chat-server/src/main/java/kickzo/stomp_chat/service/WebSocketRoomService.java
@@ -1,5 +1,7 @@
package kickzo.stomp_chat.service;
+import java.math.BigDecimal;
+
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -34,7 +36,8 @@ public void leavePage(long userId) {
* 방에 메시지 전송
*/
public void handleMessageSend(long roomId, long userId, String nickname, int role, String profileImageUrl, String content, String message) {
- ChatMessage chatMessage = new ChatMessage(roomId, userId, nickname, role, profileImageUrl, content, message);
+ long timestamp = System.currentTimeMillis(); // 현재 서버 시간
+ ChatMessage chatMessage = new ChatMessage(roomId, userId, nickname, role, profileImageUrl, content, message, timestamp);
kafkaRepository.sendChatMessage(chatMessage);
}
@@ -49,7 +52,7 @@ public void sendConnection(long userId, EventType eventType) {
/**
* Kafka에 playlistTime 전송
*/
- public void sendPlayTime (long roomId, long playTime, String playerState){
+ public void sendPlayTime (long roomId, BigDecimal playTime, String playerState){
PlayTime playTimeObject = new PlayTime(roomId, playTime, playerState);
RoomEvent roomEvent = new RoomEvent("play-time", playTimeObject);
diff --git a/src/backend/chat-server/src/main/resources/logback.xml b/src/backend/chat-server/src/main/resources/logback.xml
new file mode 100644
index 00000000..ba195308
--- /dev/null
+++ b/src/backend/chat-server/src/main/resources/logback.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ [%thread] [%date] [%level] [%file:%line] - %msg%n
+
+ kickzo-logs
+ kickzo-log
+ ap-northeast-1
+ 50
+ 30000
+ 5000
+ 0
+ ${AWS_ACCESS_KEY}
+ ${AWS_SECRET_KEY}
+
+
+
+
+
+ ${CONSOLE_LOG_PATTERN}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/backend/chat-server/src/main/resources/static/index.html b/src/backend/chat-server/src/main/resources/static/index.html
index e37f4534..9112d244 100644
--- a/src/backend/chat-server/src/main/resources/static/index.html
+++ b/src/backend/chat-server/src/main/resources/static/index.html
@@ -59,7 +59,7 @@
Room Chat
-
+
Status: Disconnected
@@ -161,14 +161,14 @@ Room Chat
console.log('Sent message:', payload);
}
- function sendPlaylistTime(playlistTime) {
+ function sendplayTime(playTime) {
if (stompClient && stompClient.connected) {
const payload = JSON.stringify({
roomId: roomId,
- playlistTime: playlistTime
+ playTime: playTime
});
stompClient.send('/app/play-time', {}, payload);
- console.log('Sent playlistTime:', payload);
+ console.log('Sent playTime:', payload);
} else {
console.error('WebSocket not connected.');
}
diff --git a/src/backend/elk/cleanup.sh b/src/backend/elk/cleanup.sh
new file mode 100644
index 00000000..8a47edf0
--- /dev/null
+++ b/src/backend/elk/cleanup.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# 모든 컨테이너 중지 및 삭제
+docker stop $(docker ps -aq)
+docker rm $(docker ps -aq)
+
+# 모든 이미지 삭제
+docker rmi $(docker images -aq)
+
+# 모든 네트워크 삭제
+docker network prune -f
+
+# 모든 볼륨 삭제
+docker volume prune -f
+
+# Docker 빌드 캐시 삭제
+docker builder prune -a -f
+
+#volume 삭제
+docker-compose down -v
+
+echo "Docker cleanup completed!"
\ No newline at end of file
diff --git a/src/backend/elk/docker-compose-es.yml b/src/backend/elk/docker-compose-es.yml
new file mode 100644
index 00000000..2372d658
--- /dev/null
+++ b/src/backend/elk/docker-compose-es.yml
@@ -0,0 +1,39 @@
+name: kickzo
+
+services:
+ elasticsearch:
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
+ container_name: elasticsearch
+ ports:
+ - "${ELASTIC_PORT}:${ELASTIC_PORT}"
+ environment:
+ - discovery.type=single-node # 단일 노드로 실행
+ - xpack.security.enabled=true
+ - bootstrap.memory_lock=true # 메모리 잠금 활성화
+ - ES_JAVA_OPTS=-Xms512m -Xmx512m # Elasticsearch JVM 메모리 설정
+ - ELASTIC_USERNAME=elastic
+ - ELASTIC_PASSWORD=test123
+ volumes:
+ - es-data:/usr/share/elasticsearch/data
+ - ./elasticsearch-init.sh:/usr/share/elasticsearch/init.sh
+ entrypoint: [ "/bin/sh", "-c", "/usr/share/elasticsearch/init.sh & /usr/local/bin/docker-entrypoint.sh" ]
+ ulimits:
+ memlock:
+ soft: -1
+ hard: -1
+ networks:
+ - kickzo-network
+ restart: always
+ healthcheck:
+ test: [ "CMD", "curl", "-f", "http://localhost:9200/_cluster/health" ]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ start_period: 10s
+
+volumes:
+ es-data:
+
+networks:
+ kickzo-network:
+ driver: bridge
\ No newline at end of file
diff --git a/src/backend/elk/docker-compose-kibana.yml b/src/backend/elk/docker-compose-kibana.yml
new file mode 100644
index 00000000..5aaba3a7
--- /dev/null
+++ b/src/backend/elk/docker-compose-kibana.yml
@@ -0,0 +1,31 @@
+name: kickzo
+
+services:
+ kibana:
+ image: docker.elastic.co/kibana/kibana:8.5.0
+ container_name: kibana
+ ports:
+ - "${KIBANA_PORT}:${KIBANA_PORT}"
+ environment:
+ ELASTICSEARCH_HOSTS: http://elasticsearch:9200
+ ELASTICSEARCH_USERNAME: kickzo
+ ELASTICSEARCH_PASSWORD: test123
+ depends_on:
+ elasticsearch:
+ condition: service_healthy
+ networks:
+ - kickzo-network
+ healthcheck:
+ test: [ "CMD", "curl", "-f", "http://localhost:5601/api/status" ]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ start_period: 10s
+
+
+volumes:
+ es-data:
+
+networks:
+ kickzo-network:
+ driver: bridge
\ No newline at end of file
diff --git a/src/backend/main-server/main/elasticsearch-init.sh b/src/backend/elk/elasticsearch-init.sh
similarity index 100%
rename from src/backend/main-server/main/elasticsearch-init.sh
rename to src/backend/elk/elasticsearch-init.sh
diff --git a/src/backend/main-server/main/docker-compose.yml b/src/backend/main-server/main/docker-compose.yml
index 6fe5844b..f05387b2 100644
--- a/src/backend/main-server/main/docker-compose.yml
+++ b/src/backend/main-server/main/docker-compose.yml
@@ -18,50 +18,13 @@ services:
SPRING_ELASTICSEARCH_PASSWORD: test123
SPRING_APPLICATION_NAME: main
depends_on:
- - elasticsearch
+ elasticsearch:
+ condition: service_healthy
+ kibana:
+ condition: service_healthy
networks:
- kickzo-network
- elasticsearch:
- image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
- container_name: elasticsearch
- ports:
- - "${ELASTIC_PORT}:${ELASTIC_PORT}"
- environment:
- - discovery.type=single-node # 단일 노드로 실행
- - xpack.security.enabled=true
- - bootstrap.memory_lock=true # 메모리 잠금 활성화
- - ES_JAVA_OPTS=-Xms512m -Xmx512m # Elasticsearch JVM 메모리 설정
- - ELASTIC_USERNAME=elastic
- - ELASTIC_PASSWORD=test123
- volumes:
- - es-data:/usr/share/elasticsearch/data
- - ./elasticsearch-init.sh:/usr/share/elasticsearch/init.sh
- entrypoint: [ "/bin/sh", "-c", "/usr/share/elasticsearch/init.sh & /usr/local/bin/docker-entrypoint.sh" ]
- ulimits:
- memlock:
- soft: -1
- hard: -1
- networks:
- - kickzo-network
-
- kibana:
- image: docker.elastic.co/kibana/kibana:8.5.0
- container_name: kibana
- ports:
- - "${KIBANA_PORT}:${KIBANA_PORT}"
- environment:
- ELASTICSEARCH_HOSTS: http://elasticsearch:9200
- ELASTICSEARCH_USERNAME: kickzo
- ELASTICSEARCH_PASSWORD: test123
- depends_on:
- - elasticsearch
- networks:
- - kickzo-network
-
-volumes:
- es-data:
-
networks:
kickzo-network:
driver: bridge
\ No newline at end of file
diff --git a/src/backend/main-server/main/kibana.yml b/src/backend/main-server/main/kibana.yml
deleted file mode 100644
index 4187b27a..00000000
--- a/src/backend/main-server/main/kibana.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-server.port: 5601
-server.host: "0.0.0.0"
-
-elasticsearch.hosts: ["http://elasticsearch:9200"]
-elasticsearch.username: "kickzo"
-elasticsearch.password: "test123"
-
-xpack.security.enabled: true
\ No newline at end of file
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/MainApplication.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/MainApplication.java
index 6bbf5032..e7d40fca 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/MainApplication.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/MainApplication.java
@@ -3,10 +3,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
+import org.springframework.scheduling.annotation.EnableScheduling;
import lombok.extern.slf4j.Slf4j;
@Slf4j
+@EnableScheduling
@SpringBootApplication
@EnableElasticsearchRepositories(basePackages = "com.kickzo.main")
public class MainApplication {
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/RoomSearchController.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/RoomSearchController.java
deleted file mode 100644
index e873a956..00000000
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/RoomSearchController.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.kickzo.main;
-
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.List;
-
-@RestController
-@RequestMapping("/api/search")
-@RequiredArgsConstructor
-public class RoomSearchController {
-
- private final RoomSearchRepository roomSearchRepository;
-
- @GetMapping()
- public List searchRooms(@RequestParam String keyword) {
- return roomSearchRepository.findByTitleContainingOrCreatorContaining(keyword, keyword);
- }
-}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/dto/response/RoomResponseDto.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/dto/response/RoomResponseDto.java
index 54a5ac13..72652a32 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/dto/response/RoomResponseDto.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/dto/response/RoomResponseDto.java
@@ -16,6 +16,7 @@ public class RoomResponseDto {
private String code;
private String title;
private String description;
+ private boolean isPublic;
private String creator;
private String profileImageUrl;
private int userCount;
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/entity/User.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/entity/User.java
new file mode 100644
index 00000000..a3ca736e
--- /dev/null
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/entity/User.java
@@ -0,0 +1,14 @@
+package com.kickzo.main.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class User {
+ private final Long userId;
+ private final String nickname;
+ private final String stateMessage;
+ private final String profileImageUrl;
+
+}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/RoomRepository.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/RoomRepository.java
index f5c8f2d2..f4f6fbea 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/RoomRepository.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/RoomRepository.java
@@ -30,5 +30,7 @@ public interface RoomRepository extends JpaRepository {
@Query(value = "SELECT EXISTS (SELECT 1 FROM room r WHERE r.id = :roomId AND r.code = :roomCode)", nativeQuery = true)
Integer existsByRoomIdAndRoomCode(@Param("roomId") Long roomId, @Param("roomCode") String roomCode);
+
+ List findByIdIn(List roomIds);
}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/UserRepository.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/UserRepository.java
index 9b149ea7..c3d29f52 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/UserRepository.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/repository/UserRepository.java
@@ -1,10 +1,16 @@
package com.kickzo.main.repository;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.List;
+
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
+import com.kickzo.main.entity.User;
+import com.kickzo.main.search.service.UserRowMapper;
import com.kickzo.main.exception.CustomErrorCode;
import com.kickzo.main.exception.CustomException;
@@ -47,4 +53,11 @@ public String findProfileImageUrlById(Long userId) {
return null;
}
}
+
+ public List findUpdatedUsers(LocalDateTime lastSyncTime) {
+ String sql = "SELECT u.id, u.nickname, u.state_message, u.profile_image_url FROM user u WHERE u.nickname_updated_at >= :nickname_updated_at";
+ MapSqlParameterSource params = new MapSqlParameterSource()
+ .addValue("nickname_updated_at", Timestamp.valueOf(lastSyncTime));
+ return jdbcTemplate.query(sql, params, new UserRowMapper());
+ }
}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/search/controller/SearchController.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/controller/SearchController.java
new file mode 100644
index 00000000..7c7d7c84
--- /dev/null
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/controller/SearchController.java
@@ -0,0 +1,62 @@
+package com.kickzo.main.search.controller;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.kickzo.main.dto.response.RoomResponseDto;
+import com.kickzo.main.repository.RoomRepository;
+import com.kickzo.main.search.document.RoomDocument;
+import com.kickzo.main.search.repository.RoomSearchRepository;
+import com.kickzo.main.search.repository.UserSearchRepository;
+import com.kickzo.main.service.MainPageService;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/search")
+@RequiredArgsConstructor
+public class SearchController {
+
+ private final RoomSearchRepository roomSearchRepository;
+ private final RoomRepository roomRepository;
+ private final UserSearchRepository userSearchRepository;
+ private final MainPageService mainPageService;
+
+ @GetMapping
+ public Map searchRoomsAndUsers(@RequestParam String keyword) {
+ Map result = new HashMap<>();
+ result.put("rooms", convertToRoomDto(keyword));
+ result.put("users", convertToUserDto(keyword));
+ return result;
+ }
+
+ private List convertToRoomDto(String keyword) {
+ return roomRepository.findByIdIn(
+ roomSearchRepository.findByTitleContainingAndIsPublic(keyword, true)
+ .stream()
+ .map(RoomDocument::getRoomId)
+ .collect(Collectors.toList())
+ ).stream().map(mainPageService::convertToDto)
+ .collect(Collectors.toList());
+ }
+
+ private List convertToUserDto(String keyword) {
+ return userSearchRepository.findByNicknameContaining(keyword)
+ .stream()
+ .map(user -> new UserResponseDto(
+ user.getUserId(),
+ user.getNickname(),
+ user.getStateMessage(),
+ user.getProfileImageUrl()
+ ))
+ .toList();
+ }
+
+ public record UserResponseDto(Long userId, String nickname, String stateMessage, String profileImageUrl) {}
+}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/RoomDocument.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/document/RoomDocument.java
similarity index 63%
rename from src/backend/main-server/main/src/main/java/com/kickzo/main/RoomDocument.java
rename to src/backend/main-server/main/src/main/java/com/kickzo/main/search/document/RoomDocument.java
index c484166e..d67a1452 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/RoomDocument.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/document/RoomDocument.java
@@ -1,4 +1,4 @@
-package com.kickzo.main;
+package com.kickzo.main.search.document;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
@@ -9,20 +9,14 @@
@Getter
@Setter
-@Document(indexName = "rooms") // Elasticsearch 인덱스 이름
+@Document(indexName = "search")
public class RoomDocument {
@Id
- private Long id;
+ private Long roomId;
@Field(type = FieldType.Text)
private String title;
@Field(type = FieldType.Boolean)
- private Boolean isPublic;
-
- @Field(type = FieldType.Integer)
- private int userCount;
-
- @Field(type = FieldType.Keyword)
- private String creator;
+ private boolean isPublic;
}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/search/document/UserDocument.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/document/UserDocument.java
new file mode 100644
index 00000000..9f337089
--- /dev/null
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/document/UserDocument.java
@@ -0,0 +1,30 @@
+package com.kickzo.main.search.document;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.annotations.Field;
+import org.springframework.data.elasticsearch.annotations.FieldType;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Document(indexName = "search")
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserDocument {
+ @Id
+ private Long userId;
+
+ @Field(type = FieldType.Text)
+ private String nickname;
+
+ @Field(type = FieldType.Text)
+ private String stateMessage;
+
+ @Field(type = FieldType.Text)
+ private String profileImageUrl;
+}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/RoomSearchRepository.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/repository/RoomSearchRepository.java
similarity index 58%
rename from src/backend/main-server/main/src/main/java/com/kickzo/main/RoomSearchRepository.java
rename to src/backend/main-server/main/src/main/java/com/kickzo/main/search/repository/RoomSearchRepository.java
index be3f0aa6..467256f2 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/RoomSearchRepository.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/repository/RoomSearchRepository.java
@@ -1,10 +1,12 @@
-package com.kickzo.main;
+package com.kickzo.main.search.repository;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
+import com.kickzo.main.search.document.RoomDocument;
+
@Repository
public interface RoomSearchRepository extends ElasticsearchRepository {
- List findByTitleContainingOrCreatorContaining(String title, String creator);
+ List findByTitleContainingAndIsPublic(String title, boolean isPublic);
}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/search/repository/UserSearchRepository.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/repository/UserSearchRepository.java
new file mode 100644
index 00000000..1da1569f
--- /dev/null
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/repository/UserSearchRepository.java
@@ -0,0 +1,13 @@
+package com.kickzo.main.search.repository;
+
+import java.util.List;
+
+import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
+import org.springframework.stereotype.Repository;
+
+import com.kickzo.main.search.document.UserDocument;
+
+@Repository
+public interface UserSearchRepository extends ElasticsearchRepository {
+ List findByNicknameContaining(String nickname);
+}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/SearchService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/SearchService.java
new file mode 100644
index 00000000..33bf3ad5
--- /dev/null
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/SearchService.java
@@ -0,0 +1,58 @@
+package com.kickzo.main.search.service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.stereotype.Component;
+
+import com.kickzo.main.entity.Room;
+import com.kickzo.main.entity.User;
+import com.kickzo.main.search.document.RoomDocument;
+import com.kickzo.main.search.document.UserDocument;
+import com.kickzo.main.search.repository.RoomSearchRepository;
+import com.kickzo.main.search.repository.UserSearchRepository;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SearchService {
+ private final UserSearchRepository userSearchRepository;
+ private final RoomSearchRepository roomSearchRepository;
+
+ public void bulkIndexUsers(List users) {
+ try {
+ List userDocuments = users.stream()
+ .map(user -> new UserDocument(user.getUserId(), user.getNickname(), user.getStateMessage(), user.getProfileImageUrl()))
+ .collect(Collectors.toList());
+
+ userSearchRepository.saveAll(userDocuments);
+ log.info("[bulkIndexUsers] ES에 {}명의 유저 저장 완료", userDocuments.size());
+ } catch (Exception e) {
+ log.error("[bulkIndexUsers] ES 저장 실패: " + e.getMessage(), e);
+ }
+ }
+
+ public void indexRoom(Room room) {
+ try {
+ if (!room.getIsPublic()) {
+ log.info("[indexRoom] 비밀방이므로 ES에 저장하지 않음: roomId = {}", room.getId());
+ return;
+ }
+ log.info("[indexRoom] ES에 방 저장: roomId = {}, title = {}", room.getId(), room.getTitle());
+
+ RoomDocument roomDocument = new RoomDocument();
+ roomDocument.setRoomId(room.getId());
+ roomDocument.setTitle(room.getTitle());
+ roomDocument.setPublic(room.getIsPublic());
+
+ roomSearchRepository.save(roomDocument);
+
+ log.info("[indexRoom] ES에 방 저장 완료!");
+ } catch (Exception e) {
+ log.error("[indexRoom] 방 저장 실패: " + e.getMessage(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/UserBatchService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/UserBatchService.java
new file mode 100644
index 00000000..a2360590
--- /dev/null
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/UserBatchService.java
@@ -0,0 +1,46 @@
+package com.kickzo.main.search.service;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.List;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import com.kickzo.main.entity.User;
+import com.kickzo.main.repository.UserRepository;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UserBatchService {
+ private final UserRepository userRepository;
+ private final SearchService searchService;
+
+ private LocalDateTime lastSyncTime = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(10);
+
+ @Scheduled(fixedRate = 300000) // 5분마다 실행 (10분 = 600000ms)
+ public void syncUsersToElasticsearch() {
+ // lastSyncTime이 NULL이면 초기값을 UTC로 설정 (중복 변환 방지)
+ if (lastSyncTime == null) {
+ lastSyncTime = LocalDateTime.now(ZoneOffset.UTC);
+ }
+
+ log.info("[syncUsersToElasticsearch] 실행됨! lastSyncTime (UTC 변환) = {}", lastSyncTime);
+ try {
+ List updatedUsers = userRepository.findUpdatedUsers(lastSyncTime);
+ log.info("[syncUsersToElasticsearch] 업데이트된 유저 개수: " + updatedUsers.size());
+ if (!updatedUsers.isEmpty()) {
+ // 새로운 lastSyncTime을 UTC 기준으로 설정
+ lastSyncTime = LocalDateTime.now(ZoneOffset.UTC);
+ searchService.bulkIndexUsers(updatedUsers);
+ lastSyncTime = LocalDateTime.now(ZoneOffset.UTC);
+ }
+ } catch (Exception e) {
+ log.error("Elasticsearch 동기화 오류: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/UserRowMapper.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/UserRowMapper.java
new file mode 100644
index 00000000..d0ed455b
--- /dev/null
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/search/service/UserRowMapper.java
@@ -0,0 +1,19 @@
+package com.kickzo.main.search.service;
+
+import org.springframework.jdbc.core.RowMapper;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import com.kickzo.main.entity.User;
+
+public class UserRowMapper implements RowMapper {
+ @Override
+ public User mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new User(
+ rs.getLong("id"),
+ rs.getString("nickname"),
+ rs.getString("state_message"),
+ rs.getString("profile_image_url")
+ );
+ }
+}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java
index b3f94f06..39453679 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/MainPageService.java
@@ -11,8 +11,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import com.kickzo.main.RoomDocument;
-import com.kickzo.main.RoomSearchRepository;
+import com.kickzo.main.search.service.SearchService;
import com.kickzo.main.dto.data.PlaylistItem;
import com.kickzo.main.dto.request.CreateRoomRequestDto;
import com.kickzo.main.dto.response.CreateRoomResponseDto;
@@ -39,7 +38,7 @@ public class MainPageService {
private final RoomRepository roomRepository;
private final RoomUserRepository roomUserRepository;
private final UserRepository userRepository;
- private final RoomSearchRepository roomSearchRepository; // Elasticsearch Repository
+ private final SearchService searchService;
private static final int MAX_ROOMS_PER_USER = 5;
private static final int ROLE_CREATOR = 0;
@@ -73,34 +72,13 @@ public CreateRoomResponseDto createRoom(Long userId, CreateRoomRequestDto reques
Room newRoom = saveNewRoom(creatorNickname, requestDto, randomCode);
saveRoomUser(newRoom.getId(), userId);
- // Elasticsearch에도 저장
- RoomDocument roomDocument = new RoomDocument();
- roomDocument.setId(newRoom.getId());
- roomDocument.setTitle(newRoom.getTitle());
- roomDocument.setCreator(newRoom.getCreator());
-
- roomSearchRepository.save(roomDocument);
+ // Elasticsearch 저장
+ searchService.indexRoom(newRoom);
return new CreateRoomResponseDto(randomCode);
}
- /**
- * 메인 페이지에서 방 list 제공
- * 1, ObjectMapper 재사용을 위한 밖에서 선언
- * 2. Playlist에서 order == 0인 URL 추출 : extractPlaylistUrl
- * 3. Room 엔티티를 DTO로 변환 : convertToDto
- * 4. getCreatorProfileImage : 생성자의 profileImageUrl 받아오기
- */
-
- private String extractPlaylistUrl(List playlistItems) {
- return playlistItems.stream()
- .filter(item -> item.getOrder() == 0)
- .map(PlaylistItem::getUrl)
- .findFirst()
- .orElse(null);
- }
-
- private RoomResponseDto convertToDto(Room room) {
+ public RoomResponseDto convertToDto(Room room) {
List playlistItems = Optional.ofNullable(room.getPlaylist())
.map(Playlist::getOrderAsList) // JSON → List 변환
.orElse(Collections.emptyList());
@@ -112,6 +90,7 @@ private RoomResponseDto convertToDto(Room room) {
.code(room.getCode())
.title(room.getTitle())
.description(room.getDescription())
+ .isPublic(room.getIsPublic())
.creator(room.getCreator())
.profileImageUrl(getCreatorProfileImage(room.getCreator()))
.userCount(room.getUserCount())
@@ -119,6 +98,19 @@ private RoomResponseDto convertToDto(Room room) {
.build();
}
+ /**
+ * 메인 페이지에서 방 list 제공
+ * 1. Playlist에서 order == 0인 URL 추출 : extractPlaylistUrl
+ * 2. getCreatorProfileImage : 생성자의 profileImageUrl 받아오기
+ */
+ private String extractPlaylistUrl(List playlistItems) {
+ return playlistItems.stream()
+ .filter(item -> item.getOrder() == 0)
+ .map(PlaylistItem::getUrl)
+ .findFirst()
+ .orElse(null);
+ }
+
private String getCreatorProfileImage(String creator) {
return userRepository.findProfileImageUrlByNickname(creator);
}
diff --git a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java
index b7cde4d9..721338e0 100644
--- a/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java
+++ b/src/backend/main-server/main/src/main/java/com/kickzo/main/service/RoomService.java
@@ -9,6 +9,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import com.kickzo.main.search.service.SearchService;
import com.kickzo.main.dto.data.PlaylistItem;
import com.kickzo.main.dto.event.RoomUpdateEvent;
import com.kickzo.main.dto.request.RoomUpdateRequestDto;
@@ -39,6 +40,7 @@ public class RoomService {
private final RoomUserRepository roomUserRepository;
private final PlaylistRepository playlistRepository;
private final UserRepository userRepository;
+ private final SearchService searchService;
private final KafkaProducerService kafkaProducerService;
private static final int ROLE_MEMBER = 2;
@@ -77,6 +79,7 @@ public void updateRoomInfo(RoomUpdateRequestDto updateRequestDto) {
}
roomRepository.save(room);
+ searchService.indexRoom(room);
RoomUpdateEvent event = new RoomUpdateEvent(roomId);
event.setUpdatedFields(updateRequestDto);
diff --git a/src/docker-compose.yml b/src/docker-compose.yml
index 9a744d07..527a37cb 100644
--- a/src/docker-compose.yml
+++ b/src/docker-compose.yml
@@ -14,8 +14,10 @@ include:
- ./backend/state-server/docker-compose.yml
- ./backend/signaling-server/docker-compose.yml
- ./backend/main-server/main/docker-compose.yml
+ - ./backend/elk/docker-compose-es.yml
+ - ./backend/elk/docker-compose-kibana.yml
- ./backend/friend-server/docker-compose.yml
networks:
kickzo-network:
- driver: bridge
+ driver: bridge
\ No newline at end of file