package com.android.trisolarisserver.component import com.android.trisolarisserver.controller.dto.RoomBoardResponse import com.android.trisolarisserver.controller.dto.RoomBoardStatus import com.android.trisolarisserver.repo.RoomRepo import com.android.trisolarisserver.repo.RoomStayRepo import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component import org.springframework.web.servlet.mvc.method.annotation.SseEmitter import java.io.IOException import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @Component class RoomBoardEvents( private val roomRepo: RoomRepo, private val roomStayRepo: RoomStayRepo ) { private val emitters: MutableMap> = ConcurrentHashMap() fun subscribe(propertyId: UUID): SseEmitter { val emitter = SseEmitter(0L) emitters.computeIfAbsent(propertyId) { CopyOnWriteArrayList() }.add(emitter) emitter.onCompletion { emitters[propertyId]?.remove(emitter) } emitter.onTimeout { emitters[propertyId]?.remove(emitter) } emitter.onError { emitters[propertyId]?.remove(emitter) } try { emitter.send(SseEmitter.event().name("room-board").data(buildSnapshot(propertyId))) } catch (_: IOException) { emitters[propertyId]?.remove(emitter) } return emitter } fun emit(propertyId: UUID) { val data = buildSnapshot(propertyId) val list = emitters[propertyId] ?: return val dead = mutableListOf() for (emitter in list) { try { emitter.send(SseEmitter.event().name("room-board").data(data)) } catch (_: IOException) { dead.add(emitter) } } if (dead.isNotEmpty()) { list.removeAll(dead.toSet()) } } @Scheduled(fixedDelayString = "25000") fun heartbeat() { emitters.forEach { (_, list) -> val dead = mutableListOf() for (emitter in list) { try { emitter.send(SseEmitter.event().name("ping").data("ok")) } catch (_: IOException) { dead.add(emitter) } } if (dead.isNotEmpty()) { list.removeAll(dead.toSet()) } } } private fun buildSnapshot(propertyId: UUID): List { val rooms = roomRepo.findByPropertyIdOrderByRoomNumber(propertyId) val occupiedRoomIds = roomStayRepo.findOccupiedRoomIds(propertyId).toHashSet() return rooms.map { room -> val status = when { room.maintenance -> RoomBoardStatus.MAINTENANCE !room.active -> RoomBoardStatus.INACTIVE occupiedRoomIds.contains(room.id) -> RoomBoardStatus.OCCUPIED else -> RoomBoardStatus.FREE } RoomBoardResponse( roomNumber = room.roomNumber, roomTypeName = room.roomType.name, status = status ) } } }