Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/develop-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
- main
- 'feature/**'
pull_request:
types: [ opened, reopened, edited ]
types: [ opened, reopened, edited, synchronize ]
jobs:
build:
name: Build and analyze
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/run_us/server/RunUsApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class RunUsApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
public class Crew extends DateAudit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "crew_id")
private Integer id;

@Column(name = "public_id", nullable = false, columnDefinition = "CHAR(13)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.run_us.server.domains.running.live.controller.model.RunningSocketResponseCode;
import com.run_us.server.domains.running.live.service.RunningLiveService;
import com.run_us.server.domains.running.common.RunningConst;
import com.run_us.server.domains.running.live.service.RunningWebsocketService;
import com.run_us.server.global.common.SuccessResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -22,7 +23,7 @@ public class RunningSocketController {

private final SimpMessagingTemplate simpMessagingTemplate;
private final RunningLiveService runningLiveService;

private final RunningWebsocketService runningWebsocketService;
/**
* 러닝 시작
* @param userId 사용자 (고유번호 세션에서 추출)
Expand All @@ -32,7 +33,7 @@ public class RunningSocketController {
public void startRunning(@UserId String userId, RunningSocketRequest.StartRunning requestDto) {
log.info("action=start_running user_id={} running_id={}", userId, requestDto.getRunningPublicId());
SuccessResponse<RunningSocketResponse.StartRunning> response =
runningLiveService.startRunning(requestDto.getRunningPublicId(), userId, requestDto.getCount());
runningWebsocketService.startRunningSession(requestDto.getRunningPublicId(), requestDto.getCount());
simpMessagingTemplate.convertAndSend(
RunningConst.RUNNING_WS_SEND_PREFIX + requestDto.getRunningPublicId(), response);
}
Expand All @@ -45,13 +46,12 @@ public void startRunning(@UserId String userId, RunningSocketRequest.StartRunnin
@MessageMapping("/users/runnings/location")
public void updateLocation(@UserId String userId, RunningSocketRequest.LocationUpdate requestDto) {
log.info("action=update_running user_id={} running_id={}", userId, requestDto.getRunningPublicId());
SuccessResponse<RunningSocketResponse.LocationUpdate> response = runningLiveService.updateLocation(
runningLiveService.updateLocation(
requestDto.getRunningPublicId(),
userId,
requestDto.getLatitude(),
requestDto.getLongitude(),
requestDto.getCount());
simpMessagingTemplate.convertAndSend(RunningConst.RUNNING_WS_SEND_PREFIX + requestDto.getRunningPublicId(), response);
}

/***
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@
import com.run_us.server.domains.running.live.service.model.LocationData;
import com.run_us.server.domains.running.live.service.model.ParticipantStatus;
import com.run_us.server.domains.running.common.RunningConst;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import java.io.Serializable;
import java.util.*;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
@RequiredArgsConstructor
public class RunningRedisRepository {

private final RedisTemplate<String, String> redisTemplate;
private final RedisTemplate<String, Serializable> serializableRedisTemplate;
private final ObjectMapper objectMapper;

public RunningRedisRepository(
RedisTemplate<String, String> redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
private final DefaultRedisScript<Boolean> updateLocationScript;

/***
* 러닝세션 참가자의 상태를 업데이트
Expand Down Expand Up @@ -60,20 +61,16 @@ public ParticipantStatus getParticipantStatus(String runningId, String userId) {
public void updateParticipantLocation(
String runningId, String userId, double latitude, double longitude, long count) {
String key = createLiveKey(runningId, userId, RunningConst.LOCATION_SUFFIX);
String currentValue = redisTemplate.opsForValue().get(key);

try {
if (currentValue != null) {
LocationData current = objectMapper.readValue(currentValue, LocationData.class);
if (count <= current.getCount()) {
return; // 기존 정보보다 과거 정보라면 업데이트 하지 않고 폐기
}
}

LocationData newLocation = new LocationData(latitude, longitude, count);
String newValue = objectMapper.writeValueAsString(newLocation);
redisTemplate.opsForValue().set(key, newValue);
serializableRedisTemplate.execute(
updateLocationScript,
List.of(key),
latitude,
longitude,
10,
count);
} catch (Exception e) {
log.error("Failed to update location", e);
throw new RuntimeException("Failed to update location", e);
}
}
Expand Down Expand Up @@ -133,7 +130,7 @@ public Set<String> getSessionParticipants(String runningId) {
* @param runningId 러닝세션 외부 노출용 ID
* @return 러닝세션 참가자 전체의 위치정보 목록
*/
private Map<String, LocationData.RunnerPos> getAllParticipantsLocations(String runningId) {
public Map<String, LocationData.RunnerPos> getAllParticipantsLocations(String runningId) {
Map<String, LocationData.RunnerPos> participantsLocations = new HashMap<>();
String pattern = createLiveKey(runningId, "*", RunningConst.LOCATION_SUFFIX);
Set<String> keys = redisTemplate.keys(pattern);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,14 @@
import com.run_us.server.domains.running.common.RunningConst;
import com.run_us.server.domains.running.run.domain.Run;
import com.run_us.server.domains.running.run.service.ParticipantService;
import com.run_us.server.domains.running.run.service.RunCommandService;
import com.run_us.server.domains.running.run.service.RunQueryService;
import com.run_us.server.domains.user.domain.User;
import com.run_us.server.global.common.SuccessResponse;
import jakarta.transaction.Transactional;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -33,12 +28,9 @@
public class RunningLiveService {

private final RunningRedisRepository runningRedisRepository;
private final RunCommandService runCommandService;
private final RunQueryService runQueryService;
private final ParticipantService participantService;
private final UpdateLocationRepository locationRepository;
private final Map<String, ScheduledExecutorService> sessionSchedulers = new ConcurrentHashMap<>();

/***
* 러닝세션 참가: 참가자 상태를 READY로 변경
* @param runningId 러닝세션 외부 노출용 ID
Expand Down Expand Up @@ -110,40 +102,7 @@ public List<LocationData.RunnerPos> endRunning(String runningId, String userId)
return Collections.emptyList();
}

/***
* 러닝세션 시작: 전체 참가자 상태를 RUN으로 변경하고, 위치 업데이트 스케줄러 시작
* @param runningId 러닝세션 외부 노출용 ID
*/
public void startRunningSession(String runningId, long count) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(
() -> runningRedisRepository.publishLocationUpdatesAll(runningId),
0,
RunningConst.UPDATE_INTERVAL,
TimeUnit.MILLISECONDS);
sessionSchedulers.put(runningId, scheduler);

Set<String> participants = runningRedisRepository.getSessionParticipants(runningId);
for (String userId : participants) {
startRunning(runningId, userId, count);
}
}

/***
* 러닝세션 종료: 전체 참가자 상태를 END로 변경하고, 위치 업데이트 스케줄러 종료
* @param runningId 러닝세션 외부 노출용 ID
*/
public void finishRunningSession(String runningId) {
ScheduledExecutorService scheduler = sessionSchedulers.remove(runningId);
if (scheduler != null) {
scheduler.shutdown();
}

Set<String> participants = runningRedisRepository.getSessionParticipants(runningId);
for (String userId : participants) {
endRunning(runningId, userId);
}
}

/***
* 러닝세션 참가자 위치 업데이트: 참가자의 위치를 업데이트하고, 이동 거리가 일정 이상일 경우 즉시 publish
Expand All @@ -153,21 +112,9 @@ public void finishRunningSession(String runningId) {
* @param longitude 경도
* @param count 송신 횟수
*/
public SuccessResponse<RunningSocketResponse.LocationUpdate> updateLocation(
public void updateLocation(
String runningId, String userId, float latitude, float longitude, long count) {
runningRedisRepository.updateParticipantLocation(runningId, userId, latitude, longitude, count);

LocationData.RunnerPos lastLocation =
runningRedisRepository.getParticipantLocation(runningId, userId);
LocationData.RunnerPos newLocation = new LocationData.RunnerPos(latitude, longitude);
locationRepository.saveLocation(createLiveKey(runningId, userId, RUNNING_PREFIX), newLocation);

if (lastLocation != null && isSignificantMove(lastLocation, newLocation)) {
runningRedisRepository.publishLocationUpdateSingle(runningId, userId, latitude, longitude);
}
return SuccessResponse.of(
RunningSocketResponseCode.UPDATE_LOCATION,
new RunningSocketResponse.LocationUpdate(userId, latitude, longitude, count));
}

/***
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.run_us.server.domains.running.live.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.run_us.server.domains.running.common.RunningConst;
import com.run_us.server.domains.running.live.controller.model.RunningSocketResponse;
import com.run_us.server.domains.running.live.controller.model.RunningSocketResponseCode;
import com.run_us.server.domains.running.live.repository.RunningRedisRepository;
import com.run_us.server.domains.running.live.service.model.LocationData;

import com.run_us.server.global.common.SuccessResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class RunningWebsocketService {

private final RunningLiveService runningLiveService;
private final RunningRedisRepository runningRedisRepository;
private final SimpMessagingTemplate simpMessagingTemplate;
private final ObjectMapper objectMapper;
private final Map<String, ScheduledExecutorService> sessionSchedulers = new ConcurrentHashMap<>();

/***
* 러닝세션 시작: 전체 참가자 상태를 RUN으로 변경하고, 위치 업데이트 스케줄러 시작
* @param runningId 러닝세션 외부 노출용 ID
*/
public SuccessResponse<RunningSocketResponse.StartRunning> startRunningSession(String runningId, long count) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(
new BroadcastPositionsTask(runningId),
0,
RunningConst.UPDATE_INTERVAL,
TimeUnit.MILLISECONDS);
sessionSchedulers.put(runningId, scheduler);
return SuccessResponse.of(
RunningSocketResponseCode.START_RUNNING,
new RunningSocketResponse.StartRunning(runningId, count));
}

/***
* 러닝세션 종료: 전체 참가자 상태를 END로 변경하고, 위치 업데이트 스케줄러 종료
* @param runningId 러닝세션 외부 노출용 ID
*/
public void finishRunningSession(String runningId) {
ScheduledExecutorService scheduler = sessionSchedulers.remove(runningId);
if (scheduler != null) {
scheduler.shutdown();
}

Set<String> participants = runningRedisRepository.getSessionParticipants(runningId);
for (String userId : participants) {
runningLiveService.endRunning(runningId, userId);
}
}

private class BroadcastPositionsTask implements Runnable {

private String runningId;

public BroadcastPositionsTask(String runningId) {
this.runningId = runningId;
}

@Override
public void run() {
Map<String, LocationData.RunnerPos> participantsLocations = runningRedisRepository.getAllParticipantsLocations(runningId);
try {
String message = objectMapper.writeValueAsString(participantsLocations);
simpMessagingTemplate.convertAndSend(RunningConst.RUNNING_WS_SEND_PREFIX + runningId, message);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
}
Loading