1+ package org .example .finalbe .domains .alert .controller ;
2+
3+ import jakarta .validation .Valid ;
4+ import lombok .RequiredArgsConstructor ;
5+ import lombok .extern .slf4j .Slf4j ;
6+ import org .example .finalbe .domains .alert .domain .AlertHistory ;
7+ import org .example .finalbe .domains .alert .dto .AlertHistoryDto ;
8+ import org .example .finalbe .domains .alert .dto .AlertStatisticsDto ;
9+ import org .example .finalbe .domains .alert .dto .AcknowledgeMultipleRequest ;
10+ import org .example .finalbe .domains .alert .dto .ResolveMultipleRequest ;
11+ import org .example .finalbe .domains .alert .repository .AlertHistoryRepository ;
12+ import org .example .finalbe .domains .alert .service .AlertNotificationService ;
13+ import org .example .finalbe .domains .common .enumdir .AlertLevel ;
14+ import org .example .finalbe .domains .common .enumdir .AlertStatus ;
15+ import org .example .finalbe .domains .common .enumdir .TargetType ;
16+ import org .example .finalbe .domains .common .exception .AlertNotFoundException ;
17+ import org .springframework .http .MediaType ;
18+ import org .springframework .http .ResponseEntity ;
19+ import org .springframework .security .access .prepost .PreAuthorize ;
20+ import org .springframework .security .core .Authentication ;
21+ import org .springframework .security .core .context .SecurityContextHolder ;
22+ import org .springframework .web .bind .annotation .*;
23+ import org .springframework .web .servlet .mvc .method .annotation .SseEmitter ;
24+
25+ import java .util .List ;
26+ import java .util .stream .Collectors ;
27+
28+ @ Slf4j
29+ @ RestController
30+ @ RequestMapping ("/api/alerts" )
31+ @ RequiredArgsConstructor
32+ @ PreAuthorize ("isAuthenticated()" )
33+ public class AlertController {
34+
35+ private final AlertNotificationService notificationService ;
36+ private final AlertHistoryRepository alertHistoryRepository ;
37+
38+ // ========== SSE 구독 ==========
39+
40+ /**
41+ * 전체 알림 구독
42+ */
43+ @ GetMapping (value = "/subscribe" , produces = MediaType .TEXT_EVENT_STREAM_VALUE )
44+ public SseEmitter subscribeAll () {
45+ log .info ("전체 알림 구독 요청" );
46+ return notificationService .subscribeAll ();
47+ }
48+
49+ /**
50+ * Equipment 알림 구독
51+ */
52+ @ GetMapping (value = "/subscribe/equipment/{id}" , produces = MediaType .TEXT_EVENT_STREAM_VALUE )
53+ public SseEmitter subscribeEquipment (@ PathVariable Long id ) {
54+ log .info ("Equipment 알림 구독 요청: equipmentId={}" , id );
55+ return notificationService .subscribeEquipment (id );
56+ }
57+
58+ /**
59+ * Rack 알림 구독
60+ */
61+ @ GetMapping (value = "/subscribe/rack/{id}" , produces = MediaType .TEXT_EVENT_STREAM_VALUE )
62+ public SseEmitter subscribeRack (@ PathVariable Long id ) {
63+ log .info ("Rack 알림 구독 요청: rackId={}" , id );
64+ return notificationService .subscribeRack (id );
65+ }
66+
67+ /**
68+ * ServerRoom 알림 구독
69+ */
70+ @ GetMapping (value = "/subscribe/serverroom/{id}" , produces = MediaType .TEXT_EVENT_STREAM_VALUE )
71+ public SseEmitter subscribeServerRoom (@ PathVariable Long id ) {
72+ log .info ("ServerRoom 알림 구독 요청: serverRoomId={}" , id );
73+ return notificationService .subscribeServerRoom (id );
74+ }
75+
76+ /**
77+ * DataCenter 알림 구독
78+ */
79+ @ GetMapping (value = "/subscribe/datacenter/{id}" , produces = MediaType .TEXT_EVENT_STREAM_VALUE )
80+ public SseEmitter subscribeDataCenter (@ PathVariable Long id ) {
81+ log .info ("DataCenter 알림 구독 요청: dataCenterId={}" , id );
82+ return notificationService .subscribeDataCenter (id );
83+ }
84+
85+ // ========== 알림 조회 ==========
86+
87+ /**
88+ * 전체 활성 알림 조회
89+ */
90+ @ GetMapping
91+ public ResponseEntity <List <AlertHistoryDto >> getAllActiveAlerts () {
92+ List <AlertHistory > alerts = alertHistoryRepository
93+ .findByStatusOrderByTriggeredAtDesc (AlertStatus .TRIGGERED );
94+
95+ List <AlertHistoryDto > dtos = alerts .stream ()
96+ .map (AlertHistoryDto ::from )
97+ .collect (Collectors .toList ());
98+
99+ return ResponseEntity .ok (dtos );
100+ }
101+
102+ /**
103+ * Equipment 알림 조회
104+ */
105+ @ GetMapping ("/equipment/{id}" )
106+ public ResponseEntity <List <AlertHistoryDto >> getEquipmentAlerts (
107+ @ PathVariable Long id ,
108+ @ RequestParam (defaultValue = "TRIGGERED" ) AlertStatus status ) {
109+
110+ List <AlertHistory > alerts = alertHistoryRepository
111+ .findByEquipmentIdAndStatusOrderByTriggeredAtDesc (id , status );
112+
113+ List <AlertHistoryDto > dtos = alerts .stream ()
114+ .map (AlertHistoryDto ::from )
115+ .collect (Collectors .toList ());
116+
117+ return ResponseEntity .ok (dtos );
118+ }
119+
120+ /**
121+ * Rack 알림 조회
122+ */
123+ @ GetMapping ("/rack/{id}" )
124+ public ResponseEntity <List <AlertHistoryDto >> getRackAlerts (
125+ @ PathVariable Long id ,
126+ @ RequestParam (defaultValue = "TRIGGERED" ) AlertStatus status ) {
127+
128+ List <AlertHistory > alerts = alertHistoryRepository
129+ .findByRackIdAndStatusOrderByTriggeredAtDesc (id , status );
130+
131+ List <AlertHistoryDto > dtos = alerts .stream ()
132+ .map (AlertHistoryDto ::from )
133+ .collect (Collectors .toList ());
134+
135+ return ResponseEntity .ok (dtos );
136+ }
137+
138+ /**
139+ * 알림 상세 조회
140+ */
141+ @ GetMapping ("/{id}" )
142+ public ResponseEntity <AlertHistoryDto > getAlertDetail (@ PathVariable Long id ) {
143+ AlertHistory alert = alertHistoryRepository .findById (id )
144+ .orElseThrow (() -> new AlertNotFoundException (id ));
145+
146+ return ResponseEntity .ok (AlertHistoryDto .from (alert ));
147+ }
148+ /**
149+ * 알림 통계 조회
150+ */
151+ @ GetMapping ("/statistics" )
152+ public ResponseEntity <AlertStatisticsDto > getStatistics () {
153+ long totalAlerts = alertHistoryRepository .count ();
154+ long triggeredAlerts = alertHistoryRepository .countByStatus (AlertStatus .TRIGGERED );
155+ long acknowledgedAlerts = alertHistoryRepository .countByStatus (AlertStatus .ACKNOWLEDGED );
156+ long resolvedAlerts = alertHistoryRepository .countByStatus (AlertStatus .RESOLVED );
157+
158+ long criticalAlerts = alertHistoryRepository .countByLevel (AlertLevel .CRITICAL );
159+ long warningAlerts = alertHistoryRepository .countByLevel (AlertLevel .WARNING );
160+
161+ long equipmentAlerts = alertHistoryRepository .countByTargetType (TargetType .EQUIPMENT );
162+ long rackAlerts = alertHistoryRepository .countByTargetType (TargetType .RACK );
163+ long serverRoomAlerts = alertHistoryRepository .countByTargetType (TargetType .SERVER_ROOM );
164+ long dataCenterAlerts = alertHistoryRepository .countByTargetType (TargetType .DATA_CENTER );
165+
166+ AlertStatisticsDto stats = new AlertStatisticsDto (
167+ totalAlerts ,
168+ triggeredAlerts ,
169+ acknowledgedAlerts ,
170+ resolvedAlerts ,
171+ criticalAlerts ,
172+ warningAlerts ,
173+ equipmentAlerts ,
174+ rackAlerts ,
175+ serverRoomAlerts ,
176+ dataCenterAlerts
177+ );
178+
179+ return ResponseEntity .ok (stats );
180+ }
181+
182+ // ========== 알림 액션 ==========
183+
184+ /**
185+ * 알림 확인
186+ */
187+ @ PostMapping ("/{id}/acknowledge" )
188+ public ResponseEntity <AlertHistoryDto > acknowledgeAlert (@ PathVariable Long id ) {
189+ Long userId = extractUserId ();
190+
191+ AlertHistory alert = alertHistoryRepository .findById (id )
192+ .orElseThrow (() -> new AlertNotFoundException (id ));
193+
194+ alert .acknowledge (userId );
195+ alertHistoryRepository .save (alert );
196+
197+ notificationService .sendAlertAcknowledged (alert );
198+
199+ log .info ("알림 확인됨: alertId={}, userId={}" , id , userId );
200+
201+ return ResponseEntity .ok (AlertHistoryDto .from (alert ));
202+ }
203+
204+ /**
205+ * 알림 해결
206+ */
207+ @ PostMapping ("/{id}/resolve" )
208+ public ResponseEntity <AlertHistoryDto > resolveAlert (@ PathVariable Long id ) {
209+ Long userId = extractUserId ();
210+
211+ AlertHistory alert = alertHistoryRepository .findById (id )
212+ .orElseThrow (() -> new AlertNotFoundException (id ));
213+
214+ alert .resolve (userId );
215+ alertHistoryRepository .save (alert );
216+
217+ notificationService .sendAlertResolved (alert );
218+
219+ log .info ("알림 해결됨: alertId={}, userId={}" , id , userId );
220+
221+ return ResponseEntity .ok (AlertHistoryDto .from (alert ));
222+ }
223+
224+ /**
225+ * 여러 알림 일괄 확인
226+ */
227+ @ PostMapping ("/acknowledge-multiple" )
228+ public ResponseEntity <List <AlertHistoryDto >> acknowledgeMultipleAlerts (
229+ @ Valid @ RequestBody AcknowledgeMultipleRequest request ) {
230+
231+ Long userId = extractUserId ();
232+
233+ List <AlertHistory > alerts = alertHistoryRepository .findAllById (request .alertIds ());
234+
235+ if (alerts .isEmpty ()) {
236+ throw new AlertNotFoundException ("요청한 알림을 찾을 수 없습니다." );
237+ }
238+
239+ alerts .forEach (alert -> {
240+ alert .acknowledge (userId );
241+ notificationService .sendAlertAcknowledged (alert );
242+ });
243+
244+ alertHistoryRepository .saveAll (alerts );
245+
246+ List <AlertHistoryDto > dtos = alerts .stream ()
247+ .map (AlertHistoryDto ::from )
248+ .collect (Collectors .toList ());
249+
250+ log .info ("여러 알림 확인됨: count={}, userId={}" , alerts .size (), userId );
251+
252+ return ResponseEntity .ok (dtos );
253+ }
254+
255+ /**
256+ * 여러 알림 일괄 해결
257+ */
258+ @ PostMapping ("/resolve-multiple" )
259+ public ResponseEntity <List <AlertHistoryDto >> resolveMultipleAlerts (
260+ @ Valid @ RequestBody ResolveMultipleRequest request ) {
261+
262+ Long userId = extractUserId ();
263+
264+ List <AlertHistory > alerts = alertHistoryRepository .findAllById (request .alertIds ());
265+
266+ if (alerts .isEmpty ()) {
267+ throw new AlertNotFoundException ("요청한 알림을 찾을 수 없습니다." );
268+ }
269+
270+ alerts .forEach (alert -> {
271+ alert .resolve (userId );
272+ notificationService .sendAlertResolved (alert );
273+ });
274+
275+ alertHistoryRepository .saveAll (alerts );
276+
277+ List <AlertHistoryDto > dtos = alerts .stream ()
278+ .map (AlertHistoryDto ::from )
279+ .collect (Collectors .toList ());
280+
281+ log .info ("여러 알림 해결됨: count={}, userId={}" , alerts .size (), userId );
282+
283+ return ResponseEntity .ok (dtos );
284+ }
285+
286+ // ========== Private Methods ==========
287+
288+ /**
289+ * JWT 토큰에서 userId 추출
290+ */
291+ private Long extractUserId () {
292+ Authentication authentication = SecurityContextHolder .getContext ().getAuthentication ();
293+
294+ if (authentication == null || !authentication .isAuthenticated ()) {
295+ throw new IllegalStateException ("인증되지 않은 사용자입니다." );
296+ }
297+
298+ String userId = authentication .getName ();
299+
300+ try {
301+ return Long .parseLong (userId );
302+ } catch (NumberFormatException e ) {
303+ throw new IllegalStateException ("유효하지 않은 사용자 ID입니다." , e );
304+ }
305+ }
306+ }
0 commit comments