Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 타이머 웹소켓 STOMP 테스트 서버로 이동 #1069

Merged
merged 10 commits into from
Feb 4, 2025
4 changes: 2 additions & 2 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'

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

// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class SchedulerConfig {

@Bean
@Primary
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(100);
Expand Down
40 changes: 28 additions & 12 deletions backend/src/main/java/site/coduo/sync/service/SchedulerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ScheduledFuture;

import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.PeriodicTrigger;
Expand All @@ -14,9 +15,11 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import site.coduo.timer.domain.Timer;
import site.coduo.timer.domain.TimerStatus;
import site.coduo.timer.repository.TimerEntity;
import site.coduo.timer.repository.TimerRepository;
import site.coduo.timer.service.TimestampRegistry;
import site.coduo.timer.service.dto.TimerResponse;
import site.coduo.websocket.PairRoomWebSocketService;
import site.coduo.websocket.message.EventAndDataMessage;

Expand All @@ -28,6 +31,7 @@ public class SchedulerService {

public static final Duration DELAY_SECOND = Duration.of(1, ChronoUnit.SECONDS);

private final SimpMessagingTemplate messagingTemplate;
private final PairRoomWebSocketService pairRoomWebSocketService;
private final ThreadPoolTaskScheduler taskScheduler;
private final SchedulerRegistry schedulerRegistry;
Expand All @@ -38,7 +42,10 @@ public void start(final String key) {
if (schedulerRegistry.isActive(key)) {
return;
}
pairRoomWebSocketService.sendAllPairRoomSessions(key, new EventAndDataMessage("timer", "start"));
messagingTemplate.convertAndSend(
"/topic/" + key + "/timer/status",
new TimerResponse(TimerStatus.START.getName())
);
if (isInitial(key)) {
final Timer timer = timerRepository.fetchTimerByAccessCode(key)
.toDomain();
Expand All @@ -65,13 +72,16 @@ private void runTimer(final String key, final Timer timer) {
stop(key, timer);
return;
}
if (pairRoomWebSocketService.hasNoConnections(key) && schedulerRegistry.has(key)) {
pauseTimer(key);
return;
}
// todo - STOMP에서 특정 topic을 구독하고 있는 사용자 수를 가져올 방법 생각
// if (pairRoomWebSocketService.hasNoConnections(key) && schedulerRegistry.has(key)) {
// pauseTimer(key);
// return;
// }
timer.decreaseRemainingTime(DELAY_SECOND.toMillis());
pairRoomWebSocketService.sendAllPairRoomSessions(key,
new EventAndDataMessage("remaining-time", String.valueOf(timer.getRemainingTime())));
messagingTemplate.convertAndSend(
"/topic/" + key + "/timer",
new TimerResponse(timer.getRemainingTime())
);
}

private void pauseTimer(final String key) {
Expand All @@ -82,11 +92,15 @@ private void pauseTimer(final String key) {

public void pause(final String key) {
pauseTimer(key);
pairRoomWebSocketService.sendAllPairRoomSessions(key, new EventAndDataMessage("timer", "pause"));
messagingTemplate.convertAndSend(
"/topic/" + key + "/timer/status",
new TimerResponse(TimerStatus.PAUSE.getName()));
}

private void stop(final String key, final Timer timer) {
pairRoomWebSocketService.sendAllPairRoomSessions(key, new EventAndDataMessage("timer", "stop"));
messagingTemplate.convertAndSend(
"/topic/" + key + "/timer/status",
new TimerResponse(TimerStatus.PAUSE.getName()));
schedulerRegistry.release(key);
final Timer initalTimer = new Timer(timer.getAccessCode(), timer.getDuration(), timer.getDuration());
timestampRegistry.register(key, initalTimer);
Expand All @@ -99,8 +113,10 @@ public void notifyTimerStatus(final WebSocketSession session, final String pairR
}

public void syncTimerWithDatabase(final String key) {
final Timer timer = timestampRegistry.get(key);
final TimerEntity timerEntity = timerRepository.fetchTimerByAccessCode(key);
timerEntity.updateTimer(timer);
if (timestampRegistry.has(key)) {
final Timer timer = timestampRegistry.get(key);
final TimerEntity timerEntity = timerRepository.fetchTimerByAccessCode(key);
timerEntity.updateTimer(timer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import jakarta.validation.Valid;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -14,17 +13,13 @@
import site.coduo.sync.service.SchedulerService;
import site.coduo.timer.controller.docs.TimerDocs;
import site.coduo.timer.service.TimerService;
import site.coduo.timer.service.dto.TimerReadResponse;
import site.coduo.timer.service.dto.TimerUpdateRequest;
import site.coduo.websocket.PairRoomWebSocketService;
import site.coduo.websocket.message.EventAndDataMessage;

@Slf4j
@RequiredArgsConstructor
@RestController
public class TimerController implements TimerDocs {

private final PairRoomWebSocketService pairRoomWebSocketService;
private final TimerService timerService;
private final SchedulerService schedulerService;

Expand All @@ -38,7 +33,6 @@ public ResponseEntity<Void> createTimerStart(@PathVariable("accessCode") final S
@PatchMapping("/{accessCode}/timer/stop")
public ResponseEntity<Void> createTimerStop(@PathVariable("accessCode") final String accessCode) {
schedulerService.pause(accessCode);

return ResponseEntity.noContent()
.build();
}
Expand All @@ -49,18 +43,7 @@ public ResponseEntity<Void> updateTimer(
@Valid @RequestBody final TimerUpdateRequest request
) {
timerService.updateTimer(accessCode, request);
pairRoomWebSocketService.sendAllPairRoomSessions(accessCode, new EventAndDataMessage("timer", "update"));

return ResponseEntity.noContent()
.build();
}

@GetMapping("/{accessCode}/timer")
public ResponseEntity<TimerReadResponse> getTimer(
@PathVariable("accessCode") final String accessCode
) {
final TimerReadResponse response = timerService.readTimer(accessCode);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import site.coduo.common.controller.response.ApiErrorResponse;
import site.coduo.timer.service.dto.TimerReadResponse;
import site.coduo.timer.service.dto.TimerUpdateRequest;

@Tag(name = "타이머 API")
Expand All @@ -34,11 +33,4 @@ ResponseEntity<Void> updateTimer(
@Parameter(description = "타이머 업데이트 요청", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE), required = true)
TimerUpdateRequest request
);

@Operation(summary = "타이머를 조회한다.")
@ApiResponse(responseCode = "200", description = "페어룸 히스토리 조회 성공", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = TimerReadResponse.class)))
@ApiResponse(responseCode = "4xx", description = "페어룸 히스토리 생성 실패", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class)))
ResponseEntity<TimerReadResponse> getTimer(
String accessCode
);
}
15 changes: 15 additions & 0 deletions backend/src/main/java/site/coduo/timer/domain/TimerStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package site.coduo.timer.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum TimerStatus {

START("start"),
PAUSE("pause"),
UPDATE("update");

private final String name;
}
15 changes: 8 additions & 7 deletions backend/src/main/java/site/coduo/timer/service/TimerService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package site.coduo.timer.service;

import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -8,26 +9,22 @@
import site.coduo.pairroom.repository.PairRoomEntity;
import site.coduo.pairroom.repository.PairRoomRepository;
import site.coduo.timer.domain.Timer;
import site.coduo.timer.domain.TimerStatus;
import site.coduo.timer.repository.TimerEntity;
import site.coduo.timer.repository.TimerRepository;
import site.coduo.timer.service.dto.TimerReadResponse;
import site.coduo.timer.service.dto.TimerResponse;
import site.coduo.timer.service.dto.TimerUpdateRequest;

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class TimerService {

private final SimpMessagingTemplate messagingTemplate;
private final TimerRepository timerRepository;
private final TimestampRegistry timestampRegistry;
private final PairRoomRepository pairRoomRepository;

public TimerReadResponse readTimer(final String accessCode) {
final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode);
final TimerEntity timerEntity = timerRepository.fetchTimerByPairRoomEntity(pairRoomEntity);
return TimerReadResponse.of(timerEntity.getId(), timerEntity.toDomain());
}

public long readTimerRemainingTime(final String accessCode) {
if (timestampRegistry.has(accessCode)) {
return timestampRegistry.get(accessCode)
Expand All @@ -49,5 +46,9 @@ public void updateTimer(final String accessCode, final TimerUpdateRequest update
);
timerEntity.updateTimer(newTimer);
timestampRegistry.register(accessCode, newTimer);
messagingTemplate.convertAndSend(
"/topic/" + accessCode + "/timer/status",
new TimerResponse(TimerStatus.UPDATE.getName())
);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package site.coduo.timer.service.dto;

public record TimerResponse(
Object data
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ public ResponseEntity<Void> createTodo(
@PathVariable("accessCode") final String accessCode,
@RequestBody @Valid final CreateTodoRequest request
) {
todoService.createTodo(accessCode, request.content());
todoService.createTodo(accessCode, request.contents());
return ResponseEntity.created(URI.create("/")).build();
}

@PatchMapping("/todos/{todoId}/contents")
public ResponseEntity<Void> updateContent(
@PathVariable("todoId") final long todoId,
@RequestBody @Valid final UpdateTodoContentRequest request) {
todoService.updateTodoContent(todoId, request.content());
todoService.updateTodoContent(todoId, request.contents());
return ResponseEntity.noContent().build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package site.coduo.todo.controller;

import java.util.List;

import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import site.coduo.todo.service.TodoService;
import site.coduo.todo.service.dto.CreateTodoRequest;
import site.coduo.todo.service.dto.TodoReadResponse;
import site.coduo.todo.service.dto.UpdateTodoContentRequest;
import site.coduo.todo.service.dto.UpdateTodoOrderRequest;

@Slf4j
@Controller
@RequiredArgsConstructor
public class TodoWebSocketController {

private final TodoService todoService;

@MessageMapping("/{accessCode}/todo/post")
@SendTo("/topic/{accessCode}/todo")
public List<TodoReadResponse> createTodo(final CreateTodoRequest request,
@DestinationVariable("accessCode") final String accessCode) {
todoService.createTodo(accessCode, request.contents());
return todoService.getAllOrderBySort(accessCode);
}

@MessageMapping("/{accessCode}/todo/update/{todoId}/order")
@SendTo("/topic/{accessCode}/todo")
public List<TodoReadResponse> modifyTodoOrder(final UpdateTodoOrderRequest request,
@DestinationVariable("todoId") Long id) {
return todoService.updateTodoSort(id, request.order());
}

@MessageMapping("/{accessCode}/todo/update/{todoId}/contents")
@SendTo("/topic/{accessCode}/todo")
public List<TodoReadResponse> modifyTodoContent(@DestinationVariable("todoId") final long todoId,
@DestinationVariable("accessCode") final String accessCode,
final UpdateTodoContentRequest request) {
todoService.updateTodoContent(todoId, request.contents());
return todoService.getAllOrderBySort(accessCode);
}

@MessageMapping("/{accessCode}/todo/update/{todoId}/checked")
@SendTo("/topic/{accessCode}/todo")
public List<TodoReadResponse> modifyTodoCheck(@DestinationVariable("todoId") final long todoId,
@DestinationVariable("accessCode") final String accessCode) {
todoService.toggleTodoChecked(todoId);
return todoService.getAllOrderBySort(accessCode);
}

@MessageMapping("/todo/{accessCode}/{todoId}/delete")
@SendTo("/topic/{accessCode}/todo")
public List<TodoReadResponse> delete(@DestinationVariable("todoId") final long todoId) {
return todoService.deleteTodo(todoId);
}
}
Loading
Loading