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
33 changes: 33 additions & 0 deletions src/main/java/com/nailagent/backend/global/sse/SseController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.nailagent.backend.global.sse;

import com.nailagent.backend.global.common.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Tag(name = "SSE", description = "실시간 알림 관련 API")
@RestController
@RequestMapping("/api/v1/sse")
@RequiredArgsConstructor
public class SseController {

private final SseService sseService;

@Operation(summary = "SSE 연결", description = "사장님 웹에서 실시간 알림을 수신하기 위한 SSE 연결을 맺습니다.")
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect() {
return sseService.connect();
}

@Operation(summary = "알림 전송", description = "AI 서버가 LLM 응답 불가 시 사장님 웹으로 알림을 전송합니다.")
@PostMapping("/notify")
public ResponseEntity<ApiResponse<Void>> notify(@Valid @RequestBody SseNotifyRequest request) {
sseService.send(request.getCustomerName(), request.getWaiting());
return ResponseEntity.ok(ApiResponse.ok(null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.nailagent.backend.global.sse;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class SseNotifyRequest {

@NotBlank(message = "고객명은 필수입니다")
@JsonProperty("customer_name")
@Schema(description = "고객명", example = "눈송이")
private String customerName;

@NotNull(message = "대기 여부는 필수입니다")
@Schema(description = "고객 응답 대기 여부", example = "true")
private Boolean waiting;
}
45 changes: 45 additions & 0 deletions src/main/java/com/nailagent/backend/global/sse/SseService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.nailagent.backend.global.sse;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;

@Slf4j
@Service
public class SseService {

private SseEmitter emitter;

public SseEmitter connect() {
if (emitter != null) {
emitter.complete();
}
SseEmitter newEmitter = new SseEmitter(Long.MAX_VALUE);
emitter = newEmitter;
newEmitter.onCompletion(() -> { if (emitter == newEmitter) emitter = null; });
newEmitter.onTimeout(() -> { if (emitter == newEmitter) emitter = null; });
return newEmitter;
}

public void send(String customerName, Boolean waiting) {
SseEmitter current = emitter;
if (current == null) {
log.warn("SSE 연결된 클라이언트 없음 - 알림 전송 불가");
return;
}
try {
current.send(SseEmitter.event()
.name("inquiry")
.data(Map.of(
"customerName", customerName,
"waiting", waiting
)));
} catch (IOException e) {
log.error("SSE 전송 실패", e);
if (emitter == current) emitter = null;
}
}
}
Loading