# Java Spring(전자정부프레임워크) 센서 시스템 개발 가이드

## 📋 프로젝트 개요

**프로젝트명**: Java Spring 기반 센서 데이터 수집 및 관리 시스템  
**기술 스택**: Java 11, Spring Boot 2.7.x, 전자정부프레임워크 4.0.x  
**아키텍처**: 계층형 아키텍처 + REST API  
**데이터베이스**: PostgreSQL, Redis  
**상태**: 개발 가이드 작성 완료  

---

## 🏗️ 시스템 아키텍처

### 전체 시스템 구성도
```
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   센서 디바이스   │────│   Java 브리지    │────│   Spring Boot   │
│   (RSNet SDK)   │    │   (RSNet SDK)   │    │   (REST API)    │
│                 │    │   (포트: 8020)   │    │   (포트: 8080)  │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                                       │
                                                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   웹 대시보드    │◄───│   PostgreSQL    │◄───│   Redis 캐시    │
│   (Thymeleaf)   │    │   (데이터 저장)   │    │   (실시간 캐시)  │
│   (포트: 8080)  │    │   (포트: 5432)   │    │   (포트: 6379)  │
└─────────────────┘    └─────────────────┘    └─────────────────┘
```

### 서비스 구성
| 서비스 | 기술 스택 | 포트 | 역할 | 상태 |
|--------|-----------|------|------|------|
| **sensor-spring-app** | Spring Boot + 전자정부프레임워크 | 8080 | 메인 웹 애플리케이션 | 🔄 개발 중 |
| **sensor-bridge** | Java + RSNet SDK | 8020 | 센서 데이터 브리지 | ✅ 기존 유지 |
| **postgres** | PostgreSQL 15 | 5432 | 메인 데이터베이스 | ✅ 기존 유지 |
| **redis** | Redis 7 | 6379 | 실시간 캐시 | ✅ 기존 유지 |

---

## 📁 프로젝트 구조

### 새로운 Spring 애플리케이션 구조
```
sensor-spring-app/
├── 📁 src/
│   ├── 📁 main/
│   │   ├── 📁 java/
│   │   │   └── 📁 com/sensor/spring/
│   │   │       ├── 📁 config/           # 설정 클래스
│   │   │       ├── 📁 controller/       # REST 컨트롤러
│   │   │       ├── 📁 service/          # 비즈니스 로직
│   │   │       ├── 📁 repository/       # 데이터 접근 계층
│   │   │       ├── 📁 entity/           # JPA 엔티티
│   │   │       ├── 📁 dto/              # 데이터 전송 객체
│   │   │       ├── 📁 exception/        # 예외 처리
│   │   │       ├── 📁 util/             # 유틸리티
│   │   │       └── 📄 SensorSpringApplication.java
│   │   ├── 📁 resources/
│   │   │   ├── 📁 static/               # 정적 리소스
│   │   │   ├── 📁 templates/            # Thymeleaf 템플릿
│   │   │   ├── 📄 application.yml       # Spring 설정
│   │   │   ├── 📄 application-dev.yml   # 개발 환경 설정
│   │   │   └── 📄 application-prod.yml  # 운영 환경 설정
│   │   └── 📁 webapp/                   # 전자정부프레임워크 설정
│   └── 📁 test/                         # 테스트 코드
├── 📄 pom.xml                           # Maven 프로젝트 설정
├── 📄 Dockerfile                        # Docker 이미지 빌드
└── 📄 README.md                         # 프로젝트 문서
```

---

## 🚀 개발 환경 설정

### 1. 필수 요구사항
- **Java**: JDK 11 이상
- **Maven**: 3.6.x 이상
- **IDE**: IntelliJ IDEA, Eclipse, VS Code
- **데이터베이스**: PostgreSQL 15, Redis 7
- **기존 시스템**: sensor-bridge, postgres, redis

### 2. Maven 프로젝트 생성

#### **pom.xml 설정**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.sensor</groupId>
    <artifactId>sensor-spring-app</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>Sensor Spring Application</name>
    <description>Spring Boot based sensor data management system</description>

    <properties>
        <java.version>11</java.version>
        <egovframework.rte.version>4.0.0</egovframework.rte.version>
        <spring-boot.version>2.7.18</spring-boot.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 전자정부프레임워크 -->
        <dependency>
            <groupId>org.egovframe.rte</groupId>
            <artifactId>org.egovframe.rte.ptl.mvc</artifactId>
            <version>${egovframework.rte.version}</version>
        </dependency>
        
        <dependency>
            <groupId>org.egovframe.rte</groupId>
            <artifactId>org.egovframe.rte.psl.dataaccess</artifactId>
            <version>${egovframework.rte.version}</version>
        </dependency>
        
        <dependency>
            <groupId>org.egovframe.rte</groupId>
            <artifactId>org.egovframe.rte.fdl.idgnr</artifactId>
            <version>${egovframework.rte.version}</version>
        </dependency>

        <!-- Database -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>

        <!-- JSON Processing -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>

        <!-- Logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

        <!-- Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            
            <plugin>
                <groupId>org.flywaydb</groupId>
                <artifactId>flyway-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

### 3. Spring Boot 메인 클래스

#### **SensorSpringApplication.java**
```java
package com.sensor.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableJpaAuditing
@EnableCaching
@EnableAsync
@EnableScheduling
public class SensorSpringApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SensorSpringApplication.class, args);
    }
}
```

---

## ⚙️ 설정 파일

### 1. application.yml (메인 설정)
```yaml
spring:
  profiles:
    active: dev
  
  application:
    name: sensor-spring-app
  
  datasource:
    url: jdbc:postgresql://localhost:5432/sensor_db
    username: postgres
    password: password
    driver-class-name: org.postgresql.Driver
    
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
        
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        
  thymeleaf:
    cache: false
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8
    mode: HTML

server:
  port: 8080
  servlet:
    context-path: /
    
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always

logging:
  level:
    com.sensor.spring: DEBUG
    org.springframework.web: DEBUG
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE
```

### 2. application-dev.yml (개발 환경)
```yaml
spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
      
  h2:
    console:
      enabled: true
      
logging:
  level:
    com.sensor.spring: DEBUG
    org.springframework.web: DEBUG
```

### 3. application-prod.yml (운영 환경)
```yaml
spring:
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate
      
logging:
  level:
    com.sensor.spring: INFO
    org.springframework.web: WARN
```

---

## 🗄️ 데이터 모델

### 1. JPA 엔티티

#### **SensorReading.java**
```java
package com.sensor.spring.entity;

import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Table(name = "sensor_readings")
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SensorReading {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotNull
    @Column(name = "device_id", nullable = false)
    private String deviceId;
    
    @Column(name = "node_id")
    private Integer nodeId;
    
    @Column(name = "temperature", precision = 5, scale = 2)
    private Double temperature;
    
    @Column(name = "humidity", precision = 5, scale = 2)
    private Double humidity;
    
    @Column(name = "longitude", precision = 10, scale = 6)
    private Double longitude;
    
    @Column(name = "latitude", precision = 10, scale = 6)
    private Double latitude;
    
    @Column(name = "recorded_time")
    private LocalDateTime recordedTime;
    
    @Column(name = "received_time")
    private LocalDateTime receivedTime;
    
    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @PrePersist
    protected void onCreate() {
        if (receivedTime == null) {
            receivedTime = LocalDateTime.now();
        }
    }
}
```

#### **Device.java**
```java
package com.sensor.spring.entity;

import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Entity
@Table(name = "devices")
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Device {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotNull
    @Column(name = "device_id", unique = true, nullable = false)
    private String deviceId;
    
    @Column(name = "name")
    private String name;
    
    @Column(name = "description")
    private String description;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "status")
    private DeviceStatus status;
    
    @Column(name = "last_seen")
    private LocalDateTime lastSeen;
    
    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
    
    public enum DeviceStatus {
        ACTIVE, INACTIVE, ERROR, MAINTENANCE
    }
}
```

### 2. DTO 클래스

#### **SensorDataRequest.java**
```java
package com.sensor.spring.dto;

import lombok.*;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SensorDataRequest {
    
    @NotNull
    private String deviceId;
    
    private Integer nodeId;
    private Double temperature;
    private Double humidity;
    private Double longitude;
    private Double latitude;
    private LocalDateTime recordedTime;
}
```

#### **SensorDataResponse.java**
```java
package com.sensor.spring.dto;

import lombok.*;
import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SensorDataResponse {
    
    private Long id;
    private String deviceId;
    private Integer nodeId;
    private Double temperature;
    private Double humidity;
    private Double longitude;
    private Double latitude;
    private LocalDateTime recordedTime;
    private LocalDateTime receivedTime;
    private LocalDateTime createdAt;
}
```

---

## 🔧 계층별 구현

### 1. Repository 계층

#### **SensorReadingRepository.java**
```java
package com.sensor.spring.repository;

import com.sensor.spring.entity.SensorReading;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public interface SensorReadingRepository extends JpaRepository<SensorReading, Long> {
    
    List<SensorReading> findByDeviceIdOrderByRecordedTimeDesc(String deviceId);
    
    Optional<SensorReading> findFirstByDeviceIdOrderByRecordedTimeDesc(String deviceId);
    
    @Query("SELECT sr FROM SensorReading sr WHERE sr.deviceId = :deviceId " +
           "AND sr.recordedTime BETWEEN :startTime AND :endTime " +
           "ORDER BY sr.recordedTime DESC")
    List<SensorReading> findByDeviceIdAndTimeRange(
        @Param("deviceId") String deviceId,
        @Param("startTime") LocalDateTime startTime,
        @Param("endTime") LocalDateTime endTime
    );
    
    @Query("SELECT COUNT(sr) FROM SensorReading sr WHERE sr.deviceId = :deviceId " +
           "AND sr.recordedTime >= :since")
    Long countByDeviceIdSince(@Param("deviceId") String deviceId, 
                              @Param("since") LocalDateTime since);
}
```

#### **DeviceRepository.java**
```java
package com.sensor.spring.repository;

import com.sensor.spring.entity.Device;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface DeviceRepository extends JpaRepository<Device, Long> {
    
    Optional<Device> findByDeviceId(String deviceId);
    
    List<Device> findByStatus(Device.DeviceStatus status);
    
    List<Device> findByLastSeenBefore(java.time.LocalDateTime time);
}
```

### 2. Service 계층

#### **SensorDataService.java**
```java
package com.sensor.spring.service;

import com.sensor.spring.dto.SensorDataRequest;
import com.sensor.spring.dto.SensorDataResponse;
import com.sensor.spring.entity.SensorReading;
import com.sensor.spring.repository.SensorReadingRepository;
import com.sensor.spring.repository.DeviceRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class SensorDataService {
    
    private final SensorReadingRepository sensorReadingRepository;
    private final DeviceRepository deviceRepository;
    private final RedisCacheService redisCacheService;
    
    @Transactional
    public SensorDataResponse saveSensorData(SensorDataRequest request) {
        log.info("Saving sensor data for device: {}", request.getDeviceId());
        
        // 디바이스 상태 업데이트
        updateDeviceLastSeen(request.getDeviceId());
        
        // 센서 데이터 저장
        SensorReading reading = SensorReading.builder()
            .deviceId(request.getDeviceId())
            .nodeId(request.getNodeId())
            .temperature(request.getTemperature())
            .humidity(request.getHumidity())
            .longitude(request.getLongitude())
            .latitude(request.getLatitude())
            .recordedTime(request.getRecordedTime())
            .receivedTime(LocalDateTime.now())
            .build();
        
        SensorReading saved = sensorReadingRepository.save(reading);
        
        // Redis 캐시 업데이트
        redisCacheService.updateLatestData(request.getDeviceId(), saved);
        
        return convertToResponse(saved);
    }
    
    @Cacheable(value = "latestData", key = "#deviceId")
    public SensorDataResponse getLatestData(String deviceId) {
        log.info("Getting latest data for device: {}", deviceId);
        
        return sensorReadingRepository
            .findFirstByDeviceIdOrderByRecordedTimeDesc(deviceId)
            .map(this::convertToResponse)
            .orElse(null);
    }
    
    @Cacheable(value = "deviceHistory", key = "#deviceId + '_' + #days")
    public List<SensorDataResponse> getDeviceHistory(String deviceId, int days) {
        log.info("Getting {} days history for device: {}", days, deviceId);
        
        LocalDateTime since = LocalDateTime.now().minusDays(days);
        
        return sensorReadingRepository
            .findByDeviceIdAndTimeRange(deviceId, since, LocalDateTime.now())
            .stream()
            .map(this::convertToResponse)
            .collect(Collectors.toList());
    }
    
    @CacheEvict(value = {"latestData", "deviceHistory"}, allEntries = true)
    public void clearCache() {
        log.info("Clearing all sensor data cache");
    }
    
    private void updateDeviceLastSeen(String deviceId) {
        deviceRepository.findByDeviceId(deviceId)
            .ifPresent(device -> {
                device.setLastSeen(LocalDateTime.now());
                device.setStatus(Device.DeviceStatus.ACTIVE);
                deviceRepository.save(device);
            });
    }
    
    private SensorDataResponse convertToResponse(SensorReading reading) {
        return SensorDataResponse.builder()
            .id(reading.getId())
            .deviceId(reading.getDeviceId())
            .nodeId(reading.getNodeId())
            .temperature(reading.getTemperature())
            .humidity(reading.getHumidity())
            .longitude(reading.getLongitude())
            .latitude(reading.getLatitude())
            .recordedTime(reading.getRecordedTime())
            .receivedTime(reading.getReceivedTime())
            .createdAt(reading.getCreatedAt())
            .build();
    }
}
```

### 3. Controller 계층

#### **SensorDataController.java**
```java
package com.sensor.spring.controller;

import com.sensor.spring.dto.SensorDataRequest;
import com.sensor.spring.dto.SensorDataResponse;
import com.sensor.spring.service.SensorDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/api/sensor-data")
@RequiredArgsConstructor
@Slf4j
@Validated
public class SensorDataController {
    
    private final SensorDataService sensorDataService;
    
    @PostMapping
    public ResponseEntity<SensorDataResponse> saveSensorData(
            @Valid @RequestBody SensorDataRequest request) {
        log.info("Received sensor data request: {}", request);
        
        try {
            SensorDataResponse response = sensorDataService.saveSensorData(request);
            return ResponseEntity.status(HttpStatus.CREATED).body(response);
        } catch (Exception e) {
            log.error("Error saving sensor data: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
    
    @GetMapping("/devices/{deviceId}/latest")
    public ResponseEntity<SensorDataResponse> getLatestData(
            @PathVariable String deviceId) {
        log.info("Getting latest data for device: {}", deviceId);
        
        SensorDataResponse response = sensorDataService.getLatestData(deviceId);
        if (response != null) {
            return ResponseEntity.ok(response);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
    
    @GetMapping("/devices/{deviceId}/history")
    public ResponseEntity<List<SensorDataResponse>> getDeviceHistory(
            @PathVariable String deviceId,
            @RequestParam(defaultValue = "7") int days) {
        log.info("Getting {} days history for device: {}", days, deviceId);
        
        List<SensorDataResponse> history = sensorDataService.getDeviceHistory(deviceId, days);
        return ResponseEntity.ok(history);
    }
    
    @DeleteMapping("/cache")
    public ResponseEntity<Void> clearCache() {
        log.info("Clearing sensor data cache");
        
        sensorDataService.clearCache();
        return ResponseEntity.ok().build();
    }
}
```

---

## 🎨 웹 인터페이스

### 1. Thymeleaf 템플릿

#### **dashboard.html**
```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>센서 데이터 대시보드</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="#">센서 모니터링 시스템</a>
        </div>
    </nav>
    
    <div class="container mt-4">
        <div class="row">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h5>실시간 센서 데이터</h5>
                    </div>
                    <div class="card-body">
                        <div id="latestData">
                            <p>데이터를 불러오는 중...</p>
                        </div>
                    </div>
                </div>
            </div>
            
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h5>온도 변화 그래프</h5>
                    </div>
                    <div class="card-body">
                        <canvas id="temperatureChart"></canvas>
                    </div>
                </div>
            </div>
        </div>
        
        <div class="row mt-4">
            <div class="col-12">
                <div class="card">
                    <div class="card-header">
                        <h5>디바이스 목록</h5>
                    </div>
                    <div class="card-body">
                        <div id="deviceList">
                            <p>디바이스 목록을 불러오는 중...</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
    <script th:src="@{/js/dashboard.js}"></script>
</body>
</html>
```

### 2. JavaScript 파일

#### **dashboard.js**
```javascript
// 센서 데이터 대시보드 JavaScript
class SensorDashboard {
    constructor() {
        this.temperatureChart = null;
        this.init();
    }
    
    init() {
        this.initTemperatureChart();
        this.loadLatestData();
        this.loadDeviceList();
        this.startRealTimeUpdates();
    }
    
    initTemperatureChart() {
        const ctx = document.getElementById('temperatureChart').getContext('2d');
        this.temperatureChart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: '온도 (°C)',
                    data: [],
                    borderColor: 'rgb(75, 192, 192)',
                    tension: 0.1
                }]
            },
            options: {
                responsive: true,
                scales: {
                    y: {
                        beginAtZero: false
                    }
                }
            }
        });
    }
    
    async loadLatestData() {
        try {
            const response = await fetch('/api/sensor-data/devices/15328737/latest');
            if (response.ok) {
                const data = await response.json();
                this.displayLatestData(data);
            }
        } catch (error) {
            console.error('Error loading latest data:', error);
        }
    }
    
    displayLatestData(data) {
        const container = document.getElementById('latestData');
        container.innerHTML = `
            <div class="row">
                <div class="col-6">
                    <strong>디바이스 ID:</strong> ${data.deviceId}<br>
                    <strong>온도:</strong> ${data.temperature}°C<br>
                    <strong>습도:</strong> ${data.humidity}%
                </div>
                <div class="col-6">
                    <strong>위도:</strong> ${data.latitude}<br>
                    <strong>경도:</strong> ${data.longitude}<br>
                    <strong>수신 시간:</strong> ${new Date(data.receivedTime).toLocaleString()}
                </div>
            </div>
        `;
    }
    
    async loadDeviceList() {
        try {
            const response = await fetch('/api/devices');
            if (response.ok) {
                const devices = await response.json();
                this.displayDeviceList(devices);
            }
        } catch (error) {
            console.error('Error loading device list:', error);
        }
    }
    
    displayDeviceList(devices) {
        const container = document.getElementById('deviceList');
        const table = `
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>디바이스 ID</th>
                        <th>이름</th>
                        <th>상태</th>
                        <th>마지막 연결</th>
                    </tr>
                </thead>
                <tbody>
                    ${devices.map(device => `
                        <tr>
                            <td>${device.deviceId}</td>
                            <td>${device.name || '-'}</td>
                            <td><span class="badge bg-${this.getStatusColor(device.status)}">${device.status}</span></td>
                            <td>${device.lastSeen ? new Date(device.lastSeen).toLocaleString() : '-'}</td>
                        </tr>
                    `).join('')}
                </tbody>
            </table>
        `;
        container.innerHTML = table;
    }
    
    getStatusColor(status) {
        switch (status) {
            case 'ACTIVE': return 'success';
            case 'INACTIVE': return 'secondary';
            case 'ERROR': return 'danger';
            case 'MAINTENANCE': return 'warning';
            default: return 'secondary';
        }
    }
    
    startRealTimeUpdates() {
        setInterval(() => {
            this.loadLatestData();
        }, 30000); // 30초마다 업데이트
    }
}

// 페이지 로드 시 대시보드 초기화
document.addEventListener('DOMContentLoaded', () => {
    new SensorDashboard();
});
```

---

## 🔄 기존 시스템 연동

### 1. Docker Compose 수정

#### **docker-compose.yml 업데이트**
```yaml
version: '3.8'

services:
  # 기존 서비스들
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: sensor_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  sensor-bridge:
    build: ./sensor-bridge
    ports:
      - "8020:8020"
    environment:
      GO_SERVER_URL: http://sensor-spring-app:8080
    depends_on:
      - postgres
      - redis

  # 새로운 Spring 애플리케이션
  sensor-spring-app:
    build: ./sensor-spring-app
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/sensor_db
      SPRING_DATASOURCE_USERNAME: postgres
      SPRING_DATASOURCE_PASSWORD: password
      SPRING_REDIS_HOST: redis
      SPRING_REDIS_PORT: 6379
    depends_on:
      - postgres
      - redis
    volumes:
      - ./sensor-spring-app/logs:/app/logs

volumes:
  postgres_data:
  redis_data:
```

### 2. 기존 Go 서버와의 차이점

| 구분 | Go 서버 | Spring Boot |
|------|---------|-------------|
| **언어** | Go | Java 11 |
| **프레임워크** | Gin | Spring Boot + 전자정부프레임워크 |
| **데이터 접근** | 직접 SQL | JPA + Hibernate |
| **캐싱** | Redis 클라이언트 | Spring Data Redis |
| **템플릿** | 없음 | Thymeleaf |
| **보안** | 기본 | Spring Security (추가 가능) |
| **모니터링** | 기본 | Spring Actuator + Micrometer |

---

## 🚀 배포 및 실행

### 1. 개발 환경 실행
```bash
# 프로젝트 디렉토리로 이동
cd sensor-spring-app

# Maven 의존성 설치
mvn clean install

# Spring Boot 애플리케이션 실행
mvn spring-boot:run
```

### 2. Docker 빌드 및 실행
```bash
# 전체 시스템 실행
docker-compose up -d

# 특정 서비스만 실행
docker-compose up -d sensor-spring-app

# 로그 확인
docker-compose logs -f sensor-spring-app
```

### 3. 운영 환경 배포
```bash
# JAR 파일 빌드
mvn clean package -DskipTests

# JAR 파일 실행
java -jar target/sensor-spring-app-1.0.0.jar \
  --spring.profiles.active=prod \
  --server.port=8080
```

---

## 📊 모니터링 및 로깅

### 1. Spring Actuator 엔드포인트
- **Health Check**: `/actuator/health`
- **Info**: `/actuator/info`
- **Metrics**: `/actuator/metrics`
- **Prometheus**: `/actuator/prometheus`

### 2. 로깅 설정
```yaml
logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: logs/sensor-spring-app.log
  level:
    com.sensor.spring: INFO
    org.springframework.web: INFO
    org.hibernate.SQL: DEBUG
```

---

## 🧪 테스트

### 1. 단위 테스트
```java
@SpringBootTest
class SensorDataServiceTest {
    
    @Autowired
    private SensorDataService sensorDataService;
    
    @Test
    void testSaveSensorData() {
        SensorDataRequest request = SensorDataRequest.builder()
            .deviceId("TEST_DEVICE")
            .temperature(25.5)
            .humidity(60.0)
            .build();
        
        SensorDataResponse response = sensorDataService.saveSensorData(request);
        
        assertNotNull(response);
        assertEquals("TEST_DEVICE", response.getDeviceId());
        assertEquals(25.5, response.getTemperature());
    }
}
```

### 2. 통합 테스트
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class SensorDataControllerIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void testSaveSensorDataEndpoint() {
        SensorDataRequest request = new SensorDataRequest();
        request.setDeviceId("TEST_DEVICE");
        request.setTemperature(25.5);
        
        ResponseEntity<SensorDataResponse> response = restTemplate.postForEntity(
            "/api/sensor-data", request, SensorDataResponse.class);
        
        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertNotNull(response.getBody());
    }
}
```

---

## 🔒 보안 설정

### 1. Spring Security 설정
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            .and()
            .csrf().disable()
            .httpBasic();
    }
}
```

### 2. CORS 설정
```java
@Configuration
public class CorsConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
```

---

## 📈 성능 최적화

### 1. 캐싱 전략
- **Redis 캐싱**: 최신 데이터, 디바이스 목록
- **JPA 2nd Level Cache**: 엔티티 캐싱
- **HTTP 응답 캐싱**: 정적 리소스

### 2. 데이터베이스 최적화
- **인덱스**: device_id, recorded_time
- **파티셔닝**: 날짜별 테이블 파티셔닝
- **Connection Pool**: HikariCP 설정

### 3. 비동기 처리
- **@Async**: 센서 데이터 저장
- **@Scheduled**: 정기적인 데이터 정리
- **WebSocket**: 실시간 데이터 스트리밍

---

## 🐛 문제 해결

### 1. 일반적인 문제

#### **포트 충돌**
```bash
# 사용 중인 포트 확인
netstat -tulpn | grep :8080

# Spring Boot 애플리케이션 중지
pkill -f "sensor-spring-app"
```

#### **데이터베이스 연결 실패**
```bash
# PostgreSQL 상태 확인
docker-compose ps postgres

# 연결 테스트
docker exec -it sensor-postgres psql -U postgres -d sensor_db
```

#### **메모리 부족**
```bash
# JVM 메모리 설정
java -Xmx2g -Xms1g -jar target/sensor-spring-app-1.0.0.jar
```

### 2. 로그 분석
```bash
# 애플리케이션 로그 확인
tail -f logs/sensor-spring-app.log

# 에러 로그 필터링
grep "ERROR" logs/sensor-spring-app.log

# 특정 디바이스 로그
grep "deviceId=15328737" logs/sensor-spring-app.log
```

---

## 📝 향후 개선 계획

### 1. 단기 개선 (1-2주)
- [ ] Spring Security 인증/인가 시스템 구축
- [ ] API 문서화 (Swagger/OpenAPI)
- [ ] 단위 테스트 커버리지 향상

### 2. 중기 개선 (1-2개월)
- [ ] 실시간 알림 시스템 (WebSocket)
- [ ] 데이터 검증 및 이상치 탐지
- [ ] 성능 모니터링 대시보드

### 3. 장기 개선 (3-6개월)
- [ ] 머신러닝 기반 예측 시스템
- [ ] 마이크로서비스 아키텍처 전환
- [ ] 클라우드 네이티브 배포

---

## 📚 참고 자료

### 1. 공식 문서
- [Spring Boot Reference](https://docs.spring.io/spring-boot/docs/current/reference/html/)
- [전자정부프레임워크 가이드](https://www.egovframe.go.kr/home/sub.do?menuNo=9)
- [Spring Data JPA](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)

### 2. 관련 라이브러리
- [Thymeleaf](https://www.thymeleaf.org/documentation.html)
- [Chart.js](https://www.chartjs.org/docs/latest/)
- [Bootstrap](https://getbootstrap.com/docs/)

---

## ✅ 체크리스트

### **개발 환경 설정**
- [x] Java 11 설치
- [x] Maven 3.6.x 설치
- [x] IDE 설정
- [x] 프로젝트 구조 생성

### **핵심 기능 구현**
- [x] JPA 엔티티 정의
- [x] Repository 계층 구현
- [x] Service 계층 구현
- [x] Controller 계층 구현
- [x] 웹 인터페이스 구현

### **설정 및 배포**
- [x] application.yml 설정
- [x] Docker 설정
- [x] 데이터베이스 연동
- [x] Redis 캐싱 설정

### **테스트 및 모니터링**
- [x] 단위 테스트 작성
- [x] 통합 테스트 작성
- [x] 로깅 설정
- [x] Actuator 설정

---

**작성일**: 2024년 12월  
**작성자**: AI Assistant  
**버전**: 1.0.0  

---

*이 가이드는 Java Spring(전자정부프레임워크)으로 센서 정보를 수집하고 관리하는 웹프로그램을 개발하기 위한 것입니다.  
추가 질문이나 명확화가 필요한 부분이 있으면 언제든 연락주세요.*
