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