package api import ( "net/http" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "sensor-server/internal/cache" "sensor-server/internal/database" "sensor-server/internal/models" "sensor-server/internal/websocket" ) // Handler API 핸들러 구조체 type Handler struct { db *gorm.DB hub *websocket.Hub } // NewHandler 새로운 핸들러 생성 func NewHandler(hub *websocket.Hub) *Handler { return &Handler{ db: database.GetDB(), hub: hub, } } // HealthCheck 헬스체크 엔드포인트 func (h *Handler) HealthCheck(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", "timestamp": time.Now(), "service": "sensor-server", }) } // ReceiveSensorData 센서 데이터 수신 func (h *Handler) ReceiveSensorData(c *gin.Context) { var request models.SensorDataRequest if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "Invalid request data: " + err.Error(), }) return } // recorded_time 문자열을 time.Time으로 파싱 recordedTime, err := time.Parse(time.RFC3339, request.RecordedTime) if err != nil { // ISO_LOCAL_DATE_TIME 형식도 시도 recordedTime, err = time.Parse("2006-01-02T15:04:05", request.RecordedTime) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "Invalid recorded_time format: " + err.Error(), }) return } } // 센서 데이터 생성 reading := &models.SensorReading{ DeviceID: request.DeviceID, NodeID: request.NodeID, Temperature: request.Temperature, Humidity: request.Humidity, Longitude: request.Longitude, Latitude: request.Latitude, // 추가 센서 데이터 필드 FloatValue: request.FloatValue, SignedInt32Value: request.SignedInt32Value, UnsignedInt32Value: request.UnsignedInt32Value, // 원시 데이터 (디버깅용) RawTem: request.RawTem, RawHum: request.RawHum, RecordedTime: recordedTime, ReceivedTime: time.Now(), } // 데이터베이스에 저장 if err := h.db.Create(reading).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "Failed to save sensor data: " + err.Error(), }) return } // Redis 캐시 업데이트 if err := cache.CacheLatestReading(request.DeviceID, reading); err != nil { // 캐시 실패는 로그만 남기고 계속 진행 c.Error(err) } // WebSocket 브로드캐스트 h.hub.BroadcastSensorData(reading) // 디바이스 정보 업데이트 h.updateDeviceInfo(request.DeviceID) c.JSON(http.StatusOK, models.SensorDataResponse{ Success: true, Message: "Sensor data received successfully", Data: reading, }) } // ReceiveExtendedSensorData 확장된 센서 데이터 수신 func (h *Handler) ReceiveExtendedSensorData(c *gin.Context) { var request models.ExtendedSensorDataRequest if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "Invalid request data: " + err.Error(), }) return } // recorded_time 문자열을 time.Time으로 파싱 recordedTime, err := time.Parse(time.RFC3339, request.RecordedTime) if err != nil { // ISO_LOCAL_DATE_TIME 형식도 시도 recordedTime, err = time.Parse("2006-01-02T15:04:05", request.RecordedTime) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "Invalid recorded_time format: " + err.Error(), }) return } } // 확장된 센서 데이터 생성 reading := &models.SensorReading{ DeviceID: request.DeviceID, NodeID: request.NodeID, Temperature: request.Temperature, Humidity: request.Humidity, Longitude: request.Longitude, Latitude: request.Latitude, // 추가 센서 데이터 필드 FloatValue: request.FloatValue, SignedInt32Value: request.SignedInt32Value, UnsignedInt32Value: request.UnsignedInt32Value, // 원시 데이터 (디버깅용) RawTem: request.RawTem, RawHum: request.RawHum, // 환경 센서 데이터 PM10: request.PM10, PM25: request.PM25, Pressure: request.Pressure, Illumination: request.Illumination, TVOC: request.TVOC, CO2: request.CO2, O2: request.O2, CO: request.CO, RecordedTime: recordedTime, ReceivedTime: time.Now(), } // 데이터베이스에 저장 if err := h.db.Create(reading).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "Failed to save extended sensor data: " + err.Error(), }) return } // Redis 캐시 업데이트 if err := cache.CacheLatestReading(request.DeviceID, reading); err != nil { // 캐시 실패는 로그만 남기고 계속 진행 c.Error(err) } // WebSocket 브로드캐스트 h.hub.BroadcastSensorData(reading) // 디바이스 정보 업데이트 h.updateDeviceInfo(request.DeviceID) c.JSON(http.StatusOK, models.SensorDataResponse{ Success: true, Message: "Extended sensor data received successfully", Data: reading, }) } // GetLatestData 최신 센서 데이터 조회 func (h *Handler) GetLatestData(c *gin.Context) { deviceID := c.Param("deviceId") if deviceID == "" { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "Device ID is required", }) return } // 먼저 캐시에서 조회 reading, err := cache.GetLatestReading(deviceID) if err == nil { c.JSON(http.StatusOK, models.SensorDataResponse{ Success: true, Data: reading, }) return } // 캐시에 없으면 데이터베이스에서 조회 var latestReading models.SensorReading if err := h.db.Where("device_id = ?", deviceID). Order("recorded_time DESC"). First(&latestReading).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "No data found for device: " + deviceID, }) return } c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "Database error: " + err.Error(), }) return } // 캐시에 저장 cache.CacheLatestReading(deviceID, &latestReading) c.JSON(http.StatusOK, models.SensorDataResponse{ Success: true, Data: &latestReading, }) } // GetHistory 히스토리 데이터 조회 func (h *Handler) GetHistory(c *gin.Context) { deviceID := c.Param("deviceId") if deviceID == "" { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "Device ID is required", }) return } // 쿼리 파라미터 파싱 limitStr := c.DefaultQuery("limit", "100") offsetStr := c.DefaultQuery("offset", "0") startTime := c.Query("start_time") endTime := c.Query("end_time") limit, err := strconv.Atoi(limitStr) if err != nil || limit <= 0 || limit > 1000 { limit = 100 } offset, err := strconv.Atoi(offsetStr) if err != nil || offset < 0 { offset = 0 } // 쿼리 빌드 query := h.db.Where("device_id = ?", deviceID) if startTime != "" { if start, err := time.Parse(time.RFC3339, startTime); err == nil { query = query.Where("recorded_time >= ?", start) } } if endTime != "" { if end, err := time.Parse(time.RFC3339, endTime); err == nil { query = query.Where("recorded_time <= ?", end) } } // 총 개수 조회 var total int64 query.Model(&models.SensorReading{}).Count(&total) // 데이터 조회 var readings []models.SensorReading if err := query.Order("recorded_time DESC"). Limit(limit). Offset(offset). Find(&readings).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "Database error: " + err.Error(), }) return } c.JSON(http.StatusOK, models.HistoryResponse{ Success: true, Data: readings, Total: total, }) } // GetDevices 디바이스 목록 조회 func (h *Handler) GetDevices(c *gin.Context) { var devices []models.Device if err := h.db.Find(&devices).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "Database error: " + err.Error(), }) return } c.JSON(http.StatusOK, models.DeviceListResponse{ Success: true, Devices: devices, }) } // updateDeviceInfo 디바이스 정보 업데이트 func (h *Handler) updateDeviceInfo(deviceID string) { var device models.Device if err := h.db.Where("device_id = ?", deviceID).First(&device).Error; err != nil { if err == gorm.ErrRecordNotFound { // 새 디바이스 생성 device = models.Device{ DeviceID: deviceID, Name: "Device " + deviceID, Status: "active", LastSeen: time.Now(), } h.db.Create(&device) } } else { // 기존 디바이스 업데이트 device.LastSeen = time.Now() device.Status = "active" h.db.Save(&device) } // 캐시 업데이트 cache.CacheDeviceInfo(&device) }