Browse Source

요구사항 수정

v2
gudae01 9 months ago
parent
commit
e82709cc66
  1. 12
      build.gradle
  2. 0
      identifier.sqlite
  3. 7
      src/main/java/org/mobidgim/mobidigimproject/MobidigimProjectApplication.java
  4. 18
      src/main/java/org/mobidgim/mobidigimproject/config/corsconfig/CorsConfig.java
  5. 10
      src/main/java/org/mobidgim/mobidigimproject/config/objectmapper/ObjectMapperConfig.java
  6. 15
      src/main/java/org/mobidgim/mobidigimproject/config/swagger/SwaggerConfig.java
  7. 12
      src/main/java/org/mobidgim/mobidigimproject/controller/Controller.java
  8. 23
      src/main/java/org/mobidgim/mobidigimproject/domain/BoardConfig.java
  9. 23
      src/main/java/org/mobidgim/mobidigimproject/domain/EventHistory.java
  10. 3
      src/main/java/org/mobidgim/mobidigimproject/model/device/DeviceDTO.java
  11. 19
      src/main/java/org/mobidgim/mobidigimproject/model/device/enums/Rs485Parity.java
  12. 18
      src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/Log.java
  13. 11
      src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/Rs485.java
  14. 2
      src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/Server.java
  15. 4
      src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/WifiSSIDS.java
  16. 16
      src/main/java/org/mobidgim/mobidigimproject/model/register/MeidDeserializer.java
  17. 25
      src/main/java/org/mobidgim/mobidigimproject/model/register/RegisterDTO.java
  18. 4
      src/main/java/org/mobidgim/mobidigimproject/model/register/enums/DeviceType.java
  19. 14
      src/main/java/org/mobidgim/mobidigimproject/model/register/enums/FourByte.java
  20. 18
      src/main/java/org/mobidgim/mobidigimproject/model/register/enums/OutputDataType.java
  21. 9
      src/main/java/org/mobidgim/mobidigimproject/model/register/enums/Protocol.java
  22. 11
      src/main/java/org/mobidgim/mobidigimproject/model/register/enums/TwoByte.java
  23. 8
      src/main/java/org/mobidgim/mobidigimproject/repository/BoardConfigRepository.java
  24. 7
      src/main/java/org/mobidgim/mobidigimproject/repository/EventHistoryRepository.java
  25. 190
      src/main/java/org/mobidgim/mobidigimproject/service/SettingService.java
  26. 27
      src/main/java/org/mobidgim/mobidigimproject/sqlite/SQLiteDialect.java
  27. 9
      src/main/java/org/mobidgim/mobidigimproject/sqlite/SQLiteIdentityColumnSupport.java
  28. 20
      src/main/resources/application.yaml
  29. 274
      src/main/resources/static/index.html

12
build.gradle

@ -26,12 +26,14 @@ repositories {
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok:1.18.30' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
implementation 'org.xerial:sqlite-jdbc:3.45.3.0'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.3'
} }

0
identifier.sqlite

7
src/main/java/org/mobidgim/mobidigimproject/MobidigimProjectApplication.java

@ -2,8 +2,15 @@ package org.mobidgim.mobidigimproject;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
@SpringBootApplication @SpringBootApplication
// (exclude = {
// DataSourceAutoConfiguration.class,
// HibernateJpaAutoConfiguration.class
//})
public class MobidigimProjectApplication { public class MobidigimProjectApplication {
public static void main(String[] args) { public static void main(String[] args) {

18
src/main/java/org/mobidgim/mobidigimproject/config/corsconfig/CorsConfig.java

@ -0,0 +1,18 @@
package org.mobidgim.mobidigimproject.config.corsconfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry reg) {
reg.addMapping("/**") // 모든 경로
.allowedOriginPatterns("*") // 모든 오리진 허용(패턴)
.allowedMethods("*") // 모든 메서드
.allowedHeaders("*") // 모든 헤더
.allowCredentials(true) // 쿠키/세션 허용
.maxAge(3600);
}
}

10
src/main/java/org/mobidgim/mobidigimproject/config/objectmapper/ObjectMapperConfig.java

@ -1,9 +1,6 @@
package org.mobidgim.mobidigimproject.config.objectmapper; package org.mobidgim.mobidigimproject.config.objectmapper;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -21,7 +18,10 @@ public class ObjectMapperConfig {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategies.SnakeCaseStrategy()); objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategies.SnakeCaseStrategy());
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
return objectMapper; return objectMapper;
} }

15
src/main/java/org/mobidgim/mobidigimproject/config/swagger/SwaggerConfig.java

@ -1,15 +0,0 @@
//package org.mobidgim.mobidigimproject.config.swagger;
//
//import com.fasterxml.jackson.databind.ObjectMapper;
//import io.swagger.v3.core.jackson.ModelResolver;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//
//@Configuration
//public class SwaggerConfig {
// @Bean
// public ModelResolver modelResolver(ObjectMapper objectMapper) {
// return new ModelResolver(objectMapper);
// }
//}
//

12
src/main/java/org/mobidgim/mobidigimproject/controller/Controller.java

@ -39,8 +39,10 @@ public class Controller {
@GetMapping("/get-device") @GetMapping("/get-device")
public ResponseEntity<DeviceDTO> getDevice() throws IOException { public ResponseEntity<DeviceDTO> getDevice() throws IOException {
DeviceDTO entity = settingService.readFileWithDevice(); if (!settingService.deviceFileExists()) { // DB에 없음
return ResponseEntity.ok(entity); return ResponseEntity.noContent().build(); // 204
}
return ResponseEntity.ok(settingService.readFileWithDevice());
} }
@PostMapping("/protocol") @PostMapping("/protocol")
@ -51,8 +53,10 @@ public class Controller {
@GetMapping("/get-protocol") @GetMapping("/get-protocol")
public ResponseEntity<RegisterDTO> getRegister() throws IOException { public ResponseEntity<RegisterDTO> getRegister() throws IOException {
RegisterDTO entity = settingService.readFileWithRegister(); if (!settingService.protocolFileExists()) { // DB에 없음
return ResponseEntity.ok(entity); return ResponseEntity.noContent().build(); // 204
}
return ResponseEntity.ok(settingService.readFileWithRegister());
} }
} }

23
src/main/java/org/mobidgim/mobidigimproject/domain/BoardConfig.java

@ -0,0 +1,23 @@
package org.mobidgim.mobidigimproject.domain;
import javax.persistence.*;
import lombok.*;
import java.sql.Timestamp;
@Entity
@Table(name = "board_config")
@Getter
@Setter
@NoArgsConstructor
public class BoardConfig {
@Id
@Column(name = "`key`", nullable = false) // key는 예약어 충돌 회피를 위해 백틱 유지
private String key;
@Column(name = "value", columnDefinition = "TEXT", nullable = false)
private String value;
}

23
src/main/java/org/mobidgim/mobidigimproject/domain/EventHistory.java

@ -0,0 +1,23 @@
package org.mobidgim.mobidigimproject.domain;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.sql.Timestamp;
@Entity
@Table(name = "event_history")
@Getter @Setter
public class EventHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "data", columnDefinition = "TEXT", nullable = false)
private String data;
@Column(name = "created_at", insertable = false, updatable = false)
private Timestamp createdAt;
}

3
src/main/java/org/mobidgim/mobidigimproject/model/device/DeviceDTO.java

@ -36,6 +36,9 @@ public class DeviceDTO {
@JsonUnwrapped @JsonUnwrapped
private Lte lteConfig; private Lte lteConfig;
@JsonUnwrapped
private Log logConfig;
@JsonProperty("WIFI_SSID") @JsonProperty("WIFI_SSID")
private List<WifiSSIDS> wifiSsid; private List<WifiSSIDS> wifiSsid;
} }

19
src/main/java/org/mobidgim/mobidigimproject/model/device/enums/Rs485Parity.java

@ -1,5 +1,6 @@
package org.mobidgim.mobidigimproject.model.device.enums; package org.mobidgim.mobidigimproject.model.device.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -14,4 +15,22 @@ public enum Rs485Parity {
public String getValue() { public String getValue() {
return value; return value;
} }
@JsonCreator
public static Rs485Parity from(String raw) { // 역직렬화 시: 문자열 받아 enum으로
if (raw == null) return null;
String v = raw.trim().toLowerCase();
switch (v) {
case "no":
case "none":
case "n":
return NONE;
case "even":
return EVEN;
case "odd":
return ODD;
default:
throw new IllegalArgumentException("Unknown parity: " + raw);
}
}
} }

18
src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/Log.java

@ -0,0 +1,18 @@
package org.mobidgim.mobidigimproject.model.device.subDTO;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Log {
@JsonProperty("log_save")
private String logSave;
@JsonProperty("log_max_size")
private Integer logMaxSize;
@JsonProperty("log_max_duration")
private Integer logMaxDuration;
}

11
src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/Rs485.java

@ -1,12 +1,17 @@
package org.mobidgim.mobidigimproject.model.device.subDTO; package org.mobidgim.mobidigimproject.model.device.subDTO;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.mobidgim.mobidigimproject.model.device.enums.Rs485Parity; import org.mobidgim.mobidigimproject.model.device.enums.Rs485Parity;
import org.mobidgim.mobidigimproject.model.device.enums.Rs485Baudrate; import org.mobidgim.mobidigimproject.model.device.enums.Rs485Baudrate;
import org.mobidgim.mobidigimproject.model.device.enums.Rs485Mode; import org.mobidgim.mobidigimproject.model.device.enums.Rs485Mode;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
@Getter @Getter
@Setter @Setter
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ -16,9 +21,11 @@ public class Rs485 {
private Rs485Baudrate rs485Baudrate; private Rs485Baudrate rs485Baudrate;
private Integer rs485DataBits; private Integer rs485Databits;
@Enumerated(EnumType.STRING)
@JsonProperty("rs485_parity")
private Rs485Parity rs485Parity; private Rs485Parity rs485Parity;
private Integer rs485StopBits; private Integer rs485Stopbits;
} }

2
src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/Server.java

@ -17,4 +17,6 @@ public class Server {
private Integer opcUaServerPort; private Integer opcUaServerPort;
private String modbusServerIp; private String modbusServerIp;
private Integer modbusServerPort; private Integer modbusServerPort;
private String lteServerIp;
private Integer lteServerPort;
} }

4
src/main/java/org/mobidgim/mobidigimproject/model/device/subDTO/WifiSSIDS.java

@ -13,8 +13,8 @@ public class WifiSSIDS {
@JsonProperty("wifi_ssid") @JsonProperty("wifi_ssid")
private String wifiSsid; private String wifiSsid;
@JsonProperty("wifi_password") @JsonProperty("wifi_passwd")
private String wifiPassword; private String wifiPasswd;
private WiFiSecurity wifiSecurity; private WiFiSecurity wifiSecurity;
} }

16
src/main/java/org/mobidgim/mobidigimproject/model/register/MeidDeserializer.java

@ -0,0 +1,16 @@
package org.mobidgim.mobidigimproject.model.register;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
public class MeidDeserializer extends JsonDeserializer<Object> {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
return node.asText(); // 숫자든 문자열이든 "문자열"로 강제
}
}

25
src/main/java/org/mobidgim/mobidigimproject/model/register/RegisterDTO.java

@ -1,8 +1,10 @@
package org.mobidgim.mobidigimproject.model.register; package org.mobidgim.mobidigimproject.model.register;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.mobidgim.mobidigimproject.model.register.enums.*; import org.mobidgim.mobidigimproject.model.register.enums.*;
@ -36,38 +38,51 @@ public class RegisterDTO {
private AnalogPort di3; private AnalogPort di3;
@JsonProperty("can_input")
private CanInput canInput; private CanInput canInput;
private DeviceType deviceType; @JsonProperty("dev_type")
private DeviceType devType;
@JsonProperty("dr_on")
private DR drOn; private DR drOn;
private Equipment equipment; private Equipment equipment;
private TwoByte twoByte; @JsonProperty("two_byte_order")
private TwoByte twoByteOrder;
private FourByte fourByte; @JsonProperty("four_byte_order")
private FourByte fourByteOrder;
private Protocol protocol; private Protocol protocol;
private SpeedDataSource speedDataSource; @JsonProperty("speed_data")
private SpeedDataSource speedData;
private String version; private String version;
@JsonProperty("equipment_id")
private String equipmentId; private String equipmentId;
@JsonProperty("MEID")
@JsonDeserialize(using = MeidDeserializer.class)
private String meid; private String meid;
@JsonProperty("heading_on")
private HeadingOn headingOn; private HeadingOn headingOn;
@JsonProperty("heading_imu_on")
private HeadingImuOn headingImuOn; private HeadingImuOn headingImuOn;
private FixmodeOn fixmodeOn; @JsonProperty("fix_mode_on")
private FixmodeOn fixModeOn;
@JsonProperty("MODBUS") @JsonProperty("MODBUS")
private List<ModbusField> MODBUS; private List<ModbusField> MODBUS;
@JsonProperty("OPC_UA") @JsonProperty("OPC_UA")
@JsonAlias({"OPC_UA","OPC-UA"})
private List<OpcUaField> OPC_UA; private List<OpcUaField> OPC_UA;
@JsonProperty("CAN") @JsonProperty("CAN")

4
src/main/java/org/mobidgim/mobidigimproject/model/register/enums/DeviceType.java

@ -7,11 +7,11 @@ import lombok.AllArgsConstructor;
public enum DeviceType { public enum DeviceType {
TIOT("TIOT"), TIOT("TIOT"),
RTLS("RTLS"); RTLS("RTLS");
private final String Device; private final String Dev;
@JsonValue @JsonValue
public String getType() { public String getType() {
return Device; return Dev;
} }
} }

14
src/main/java/org/mobidgim/mobidigimproject/model/register/enums/FourByte.java

@ -1,5 +1,6 @@
package org.mobidgim.mobidigimproject.model.register.enums; package org.mobidgim.mobidigimproject.model.register.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -15,4 +16,17 @@ public enum FourByte {
public String getFourByte() { public String getFourByte() {
return fourByte; return fourByte;
} }
@JsonCreator
public static FourByte from(String v) {
if (v == null) return null;
String x = v.trim().toLowerCase().replace("_","").replace("-","");
switch (x) {
case "little": return LITTLE;
case "littleswap": return LITTLE_SWAP;
case "bigswap": return BIG_SWAP;
case "big": return BIG;
default: throw new IllegalArgumentException("Unknown FourByte: " + v);
}
}
} }

18
src/main/java/org/mobidgim/mobidigimproject/model/register/enums/OutputDataType.java

@ -1,5 +1,6 @@
package org.mobidgim.mobidigimproject.model.register.enums; package org.mobidgim.mobidigimproject.model.register.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -15,4 +16,21 @@ public enum OutputDataType {
public String getOdt() { public String getOdt() {
return odt; return odt;
} }
@JsonCreator
public static OutputDataType from(String v){
if (v==null) return null;
String x = v.trim().toLowerCase();
switch (x){
case "integer": return integer;
case "float64":
case "float":
case "double": return float64;
case "boolean":
case "bool": return bool;
case "string":
case "str": return String;
default: throw new IllegalArgumentException("Unknown OutputDataType: " + v);
}
}
} }

9
src/main/java/org/mobidgim/mobidigimproject/model/register/enums/Protocol.java

@ -1,5 +1,6 @@
package org.mobidgim.mobidigimproject.model.register.enums; package org.mobidgim.mobidigimproject.model.register.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -15,4 +16,12 @@ public enum Protocol {
public String getProtocol() { public String getProtocol() {
return protocol; return protocol;
} }
@JsonCreator
public static Protocol from(String v) {
if (v == null) return null;
return Protocol.valueOf(
v.trim().toUpperCase().replace('-', '_').replace(' ', '_')
);
}
} }

11
src/main/java/org/mobidgim/mobidigimproject/model/register/enums/TwoByte.java

@ -1,5 +1,6 @@
package org.mobidgim.mobidigimproject.model.register.enums; package org.mobidgim.mobidigimproject.model.register.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -13,4 +14,14 @@ public enum TwoByte {
public String getTwoByte() { public String getTwoByte() {
return twoByte; return twoByte;
} }
@JsonCreator
public static TwoByte from(String v) {
if (v == null) return null;
switch (v.trim().toLowerCase()) {
case "little": return LITTLE;
case "big": return BIG;
default: throw new IllegalArgumentException("Unknown TwoByte: " + v);
}
}
} }

8
src/main/java/org/mobidgim/mobidigimproject/repository/BoardConfigRepository.java

@ -0,0 +1,8 @@
package org.mobidgim.mobidigimproject.repository;
import org.mobidgim.mobidigimproject.domain.BoardConfig;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardConfigRepository extends JpaRepository<BoardConfig, String> {
}

7
src/main/java/org/mobidgim/mobidigimproject/repository/EventHistoryRepository.java

@ -0,0 +1,7 @@
package org.mobidgim.mobidigimproject.repository;
import org.mobidgim.mobidigimproject.domain.EventHistory;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EventHistoryRepository extends JpaRepository<EventHistory, Long> {
}

190
src/main/java/org/mobidgim/mobidigimproject/service/SettingService.java

@ -1,193 +1,145 @@
package org.mobidgim.mobidigimproject.service; package org.mobidgim.mobidigimproject.service;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import lombok.RequiredArgsConstructor;
import org.mobidgim.mobidigimproject.domain.BoardConfig;
import org.mobidgim.mobidigimproject.model.device.DeviceDTO; import org.mobidgim.mobidigimproject.model.device.DeviceDTO;
import org.mobidgim.mobidigimproject.model.register.RegisterDTO; import org.mobidgim.mobidigimproject.model.register.RegisterDTO;
import org.mobidgim.mobidigimproject.model.register.enums.CanInput; import org.mobidgim.mobidigimproject.model.register.enums.CanInput;
import org.mobidgim.mobidigimproject.model.register.enums.Protocol; import org.mobidgim.mobidigimproject.model.register.enums.Protocol;
import org.mobidgim.mobidigimproject.model.register.sebDTO.CanField; import org.mobidgim.mobidigimproject.repository.BoardConfigRepository;
import org.mobidgim.mobidigimproject.model.register.sebDTO.ModbusField;
import org.mobidgim.mobidigimproject.model.register.sebDTO.OpcUaField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.swing.text.html.parser.Parser; import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.util.Collections;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor
public class SettingService { public class SettingService {
private final BoardConfigRepository boardConfigRepository;
private final ObjectMapper mapper; private final ObjectMapper mapper;
private static final String DEFAULT_DEVICE_FILENAME = "deviceConfig.json"; private static final String DEFAULT_DEVICE_FILENAME = "deviceConfig.json";
private static final String DEFAULT_PROTOCOL_FILENAME = "protocolConfig.json"; private static final String DEFAULT_PROTOCOL_FILENAME = "protocolConfig.json";
private static final String TEMP_DIRECTORY = System.getProperty("user.home") + "/config"; private static final String KEY_DEVICE = "device_config";
private static final String KEY_PROTOCOL = "protocol_config";
@Autowired @PostConstruct
public SettingService(ObjectMapper mapper) { void initMapper() {
this.mapper = mapper; mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
this.mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
} }
// 디렉토리 생성 메서드 /* ===================== Device ===================== */
private void ensureDirectoryExists() throws IOException {
Path dirPath = Paths.get(TEMP_DIRECTORY);
if (!Files.exists(dirPath)) {
Files.createDirectory(dirPath);
}
}
// Device 파일 저장 (기본 파일명 사용)
public void saveFileWithDevice(DeviceDTO request) throws IOException { public void saveFileWithDevice(DeviceDTO request) throws IOException {
saveFileWithDevice(request, DEFAULT_DEVICE_FILENAME); saveFileWithDevice(request, DEFAULT_DEVICE_FILENAME);
} }
// Device 파일 저장 (사용자 지정 파일명)
public void saveFileWithDevice(DeviceDTO request, String filename) throws IOException { public void saveFileWithDevice(DeviceDTO request, String filename) throws IOException {
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); try {
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request); String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
BoardConfig row = new BoardConfig();
ensureDirectoryExists(); row.setKey(KEY_DEVICE);
row.setValue(json);
String filePath = TEMP_DIRECTORY + "/" + filename; boardConfigRepository.save(row); // upsert (PK=`key`)
Files.write(Paths.get(filePath), json.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) {
throw new IOException("Failed to save device_config to DB", e);
}
} }
// Device 파일 읽기 (기본 파일명 사용)
public DeviceDTO readFileWithDevice() throws IOException { public DeviceDTO readFileWithDevice() throws IOException {
return readFileWithDevice(DEFAULT_DEVICE_FILENAME); return readFileWithDevice(DEFAULT_DEVICE_FILENAME);
} }
// Device 파일 읽기 (사용자 지정 파일명)
public DeviceDTO readFileWithDevice(String filename) throws IOException { public DeviceDTO readFileWithDevice(String filename) throws IOException {
String filePath = TEMP_DIRECTORY + "/" + filename; try {
Path path = Paths.get(filePath); return boardConfigRepository.findById(KEY_DEVICE)
.map(bc -> {
if (!Files.exists(path)) { try { return mapper.readValue(bc.getValue(), DeviceDTO.class); }
throw new IOException("Device configuration file not found: " + filename); catch (Exception ex) { throw new RuntimeException(ex); }
})
.orElseGet(DeviceDTO::new); // 없으면 빈 객체
} catch (RuntimeException re) {
Throwable cause = (re.getCause() != null ? re.getCause() : re);
throw new IOException("Failed to read device_config from DB", cause);
} }
String jsonString = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper.readValue(jsonString, DeviceDTO.class);
} }
// Device 파일 존재 여부 확인
public boolean deviceFileExists() { public boolean deviceFileExists() {
return deviceFileExists(DEFAULT_DEVICE_FILENAME); return deviceFileExists(DEFAULT_DEVICE_FILENAME);
} }
public boolean deviceFileExists(String filename) { public boolean deviceFileExists(String filename) {
String filePath = TEMP_DIRECTORY + "/" + filename; return boardConfigRepository.existsById(KEY_DEVICE);
return Files.exists(Paths.get(filePath));
} }
// Protocol 파일 저장 (기본 파일명 사용) /* ===================== Protocol ===================== */
public RegisterDTO saveFileWithRegister(RegisterDTO request) throws IOException { public RegisterDTO saveFileWithRegister(RegisterDTO request) throws IOException {
return saveFileWithRegister(request, DEFAULT_PROTOCOL_FILENAME); return saveFileWithRegister(request, DEFAULT_PROTOCOL_FILENAME);
} }
// Protocol 파일 저장 (사용자 지정 파일명)
public RegisterDTO saveFileWithRegister(RegisterDTO request, String filename) throws IOException { public RegisterDTO saveFileWithRegister(RegisterDTO request, String filename) throws IOException {
// 프로토콜에 따른 데이터 정리 // 기존 정리 로직 유지
if (request.getProtocol() != Protocol.OPC_UA) { if (request.getProtocol() != Protocol.OPC_UA) request.setOPC_UA(null);
request.setOPC_UA(null); if (request.getProtocol() != Protocol.MODBUS) request.setMODBUS(null);
if (request.getCanInput() != CanInput.ON) request.setCAN(null);
try {
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
BoardConfig row = new BoardConfig();
row.setKey(KEY_PROTOCOL);
row.setValue(json);
boardConfigRepository.save(row);
return request;
} catch (Exception e) {
throw new IOException("Failed to save protocol_config to DB", e);
} }
if (request.getProtocol() != Protocol.MODBUS) {
request.setMODBUS(null);
}
if (request.getCanInput() != CanInput.ON) {
request.setCAN(null);
}
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
ensureDirectoryExists();
String filePath = TEMP_DIRECTORY + "/" + filename;
Path path = Paths.get(filePath);
Files.write(path, json.getBytes(StandardCharsets.UTF_8));
return request;
} }
// Protocol 파일 읽기 (기본 파일명 사용)
public RegisterDTO readFileWithRegister() throws IOException { public RegisterDTO readFileWithRegister() throws IOException {
return readFileWithRegister(DEFAULT_PROTOCOL_FILENAME); return readFileWithRegister(DEFAULT_PROTOCOL_FILENAME);
} }
// Protocol 파일 읽기 (사용자 지정 파일명)
public RegisterDTO readFileWithRegister(String filename) throws IOException { public RegisterDTO readFileWithRegister(String filename) throws IOException {
String filePath = TEMP_DIRECTORY + "/" + filename; try {
Path path = Paths.get(filePath); return boardConfigRepository.findById(KEY_PROTOCOL)
.map(bc -> {
if (!Files.exists(path)) { try { return mapper.readValue(bc.getValue(), RegisterDTO.class); }
throw new IOException("Protocol configuration file not found: " + filename); catch (Exception ex) { throw new RuntimeException(ex); }
})
.orElseGet(RegisterDTO::new);
} catch (RuntimeException re) {
Throwable cause = (re.getCause() != null ? re.getCause() : re);
throw new IOException("Failed to read protocol_config from DB", cause);
} }
String jsonString = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
RegisterDTO registerDTO = mapper.readValue(jsonString, RegisterDTO.class);
return registerDTO;
} }
// Protocol 파일 존재 여부 확인
public boolean protocolFileExists() { public boolean protocolFileExists() {
return protocolFileExists(DEFAULT_PROTOCOL_FILENAME); return protocolFileExists(DEFAULT_PROTOCOL_FILENAME);
} }
public boolean protocolFileExists(String filename) { public boolean protocolFileExists(String filename) {
String filePath = TEMP_DIRECTORY + "/" + filename; return boardConfigRepository.existsById(KEY_PROTOCOL);
return Files.exists(Paths.get(filePath));
} }
// temp 디렉토리의 모든 JSON 파일 목록 조회 /* ===================== 목록 대체 ===================== */
public List<String> getAvailableDeviceFiles() throws IOException { public List<String> getAvailableDeviceFiles() throws IOException {
return getAvailableFiles("device"); return boardConfigRepository.existsById(KEY_DEVICE)
? Collections.singletonList(DEFAULT_DEVICE_FILENAME)
: Collections.emptyList();
} }
public List<String> getAvailableProtocolFiles() throws IOException { public List<String> getAvailableProtocolFiles() throws IOException {
return getAvailableFiles("protocol"); return boardConfigRepository.existsById(KEY_PROTOCOL)
} ? Collections.singletonList(DEFAULT_PROTOCOL_FILENAME)
: Collections.emptyList();
private List<String> getAvailableFiles(String type) throws IOException {
ensureDirectoryExists();
Path dirPath = Paths.get(TEMP_DIRECTORY);
return Files.list(dirPath)
.filter(path -> path.toString().endsWith(".json"))
.map(path -> path.getFileName().toString())
.filter(filename -> {
try {
if ("device".equals(type)) {
readFileWithDevice(filename);
return true;
} else if ("protocol".equals(type)) {
readFileWithRegister(filename);
return true;
}
return false;
} catch (Exception e) {
return false; // 파일이 해당 타입에 맞지 않으면 제외
}
})
.collect(Collectors.toList());
} }
} }

27
src/main/java/org/mobidgim/mobidigimproject/sqlite/SQLiteDialect.java

@ -0,0 +1,27 @@
package org.mobidgim.mobidigimproject.sqlite;
import java.sql.Types;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.identity.IdentityColumnSupport;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.dialect.function.*;
public class SQLiteDialect extends Dialect {
public SQLiteDialect() {
registerColumnType(Types.INTEGER, "integer");
registerColumnType(Types.BIGINT, "bigint");
registerColumnType(Types.VARCHAR, "varchar");
registerColumnType(Types.LONGVARCHAR, "longvarchar");
registerColumnType(Types.BOOLEAN, "boolean");
registerColumnType(Types.BLOB, "blob");
registerColumnType(Types.CLOB, "text");
registerColumnType(Types.TIMESTAMP, "datetime");
registerFunction("concat", new VarArgsSQLFunction(StandardBasicTypes.STRING, "", "||", ""));
registerFunction("substr", new StandardSQLFunction("substr", StandardBasicTypes.STRING));
registerFunction("substring", new StandardSQLFunction("substr", StandardBasicTypes.STRING));
registerFunction("mod", new SQLFunctionTemplate(StandardBasicTypes.INTEGER, "?1 % ?2"));
}
@Override public IdentityColumnSupport getIdentityColumnSupport() { return new SQLiteIdentityColumnSupport(); }
}

9
src/main/java/org/mobidgim/mobidigimproject/sqlite/SQLiteIdentityColumnSupport.java

@ -0,0 +1,9 @@
package org.mobidgim.mobidigimproject.sqlite;
import org.hibernate.dialect.identity.IdentityColumnSupportImpl;
public class SQLiteIdentityColumnSupport extends IdentityColumnSupportImpl {
@Override public boolean supportsIdentityColumns() { return true; }
@Override public String getIdentityColumnString(int type) { return "integer"; }
@Override public boolean hasDataTypeInIdentityColumn() { return false; }
}

20
src/main/resources/application.yaml

@ -0,0 +1,20 @@
spring:
datasource:
url: "jdbc:sqlite:${user.home}/db/dynamic_data.db?busy_timeout=5000"
driver-class-name: "org.sqlite.JDBC"
hikari:
maximum-pool-size: 1
# username/password 필요 없음
jpa:
hibernate:
ddl-auto: none # 초기엔 update, 이후 none 권장
properties:
hibernate:
format_sql: true
# 커스텀 SQLite Dialect 경로 (아래 3번)
database-platform: "org.mobidgim.mobidigimproject.sqlite.SQLiteDialect"
server:
port: 8080

274
src/main/resources/static/index.html

@ -16,6 +16,7 @@
<button data-page="ssid">SSID</button> <button data-page="ssid">SSID</button>
<button data-page="network">Network &amp; Server</button> <button data-page="network">Network &amp; Server</button>
<button data-page="io">I/O</button> <button data-page="io">I/O</button>
<button data-page="log">Log</button>
</div> </div>
</details> </details>
<details class="nav"> <details class="nav">
@ -60,8 +61,8 @@
<div class="file-upload-wrapper-device" <div class="file-upload-wrapper-device"
data-file-type="device" data-file-type="device"
data-expected-filename="deviceConfig.json" data-expected-filename="deviceConfig.json"
data-upload-url="http://localhost:8080/setting/device" data-upload-url="/setting/device"
data-get-url="http://localhost:8080/setting/get-device"> data-get-url="/setting/get-device">
<input type="file" class="upload-input-device" accept=".json" style="display: none;"> <input type="file" class="upload-input-device" accept=".json" style="display: none;">
<button class="select-file-btn-device">Load Local Device File</button> <button class="select-file-btn-device">Load Local Device File</button>
</div> </div>
@ -98,17 +99,19 @@
id="update_server_port"/></div> id="update_server_port"/></div>
<div class="grid-3-col"><span>DGPS Server</span> <input id="rtcm_server_ip" placeholder="ex)123.123.123.123"/><input <div class="grid-3-col"><span>DGPS Server</span> <input id="rtcm_server_ip" placeholder="ex)123.123.123.123"/><input
id="rtcm_server_port"/></div> id="rtcm_server_port"/></div>
<div class="grid-3-col"><span>OPC-UA</span> <input id="opc_ua_server_ip" placeholder="ex)123.123.123.123"/><input <div class="grid-3-col"><span>OPC_UA</span> <input id="opc_ua_server_ip" placeholder="ex)123.123.123.123"/><input
id="opc_ua_server_port"/></div> id="opc_ua_server_port"/></div>
<div class="grid-3-col"><span>MODBUS</span> <input id="modbus_server_ip" placeholder="ex)123.123.123.123"/><input <div class="grid-3-col"><span>MODBUS</span> <input id="modbus_server_ip" placeholder="ex)123.123.123.123"/><input
id="modbus_server_port"/></div> id="modbus_server_port"/></div>
<div class="grid-3-col"><span>LTE Server</span> <input id="lte_server_ip" placeholder="ex)123.123.123.123"/><input
id="lte_server_port"/></div>
</div> </div>
<div class="actions"> <div class="actions">
<div class="file-upload-wrapper-device" <div class="file-upload-wrapper-device"
data-file-type="device" data-file-type="device"
data-expected-filename="deviceConfig.json" data-expected-filename="deviceConfig.json"
data-upload-url="http://localhost:8080/setting/device" data-upload-url="/setting/device"
data-get-url="http://localhost:8080/setting/get-device"> data-get-url="/setting/get-device">
<input type="file" class="upload-input-device" accept=".json" style="display: none;"> <input type="file" class="upload-input-device" accept=".json" style="display: none;">
<button class="select-file-btn-device">Load Local Device File</button> <button class="select-file-btn-device">Load Local Device File</button>
</div> </div>
@ -169,14 +172,14 @@
</select></div> </select></div>
<div class="io-row"><span class="label-inline">Parity</span><span <div class="io-row"><span class="label-inline">Parity</span><span
class="colon">:</span><select id="rs485_parity"> class="colon">:</span><select id="rs485_parity">
<option value="">NONE</option> <option value="no">NONE</option>
<option value="odd">ODD</option> <option value="odd">ODD</option>
<option value="even">EVEN</option> <option value="even">EVEN</option>
</select></div> </select></div>
<div class="io-row"><span class="label-inline">Data bits</span><span <div class="io-row"><span class="label-inline">Data bits</span><span
class="colon">:</span><input id="rs485_data_bits"></div> class="colon">:</span><input id="rs485_databits"></div>
<div class="io-row"><span class="label-inline">Stop bits</span><span <div class="io-row"><span class="label-inline">Stop bits</span><span
class="colon">:</span><input id="rs485_stop_bits"></div> class="colon">:</span><input id="rs485_stopbits"></div>
</div> </div>
</div> </div>
<!-- CAN --> <!-- CAN -->
@ -239,8 +242,38 @@
<div class="file-upload-wrapper-device" <div class="file-upload-wrapper-device"
data-file-type="device" data-file-type="device"
data-expected-filename="deviceConfig.json" data-expected-filename="deviceConfig.json"
data-upload-url="http://localhost:8080/setting/device" data-upload-url="/setting/device"
data-get-url="http://localhost:8080/setting/get-device"> data-get-url="/setting/get-device">
<input type="file" class="upload-input-device" accept=".json" style="display: none;">
<button class="select-file-btn-device">Load Local Device File</button>
</div>
<button class="download-device-file">Download Local Device File</button>
</div>
</section>
<!-- ▽ LOG PAGE ▽ -->
<section id="page-log" class="page" style="display:none">
<div class="card">
<h3>Log Configuration</h3>
<div class="io-group">
<div class="io-fieldset">
<div class="io-row"><span class="label-inline">Log Save</span><span class="colon">:</span>
<select id="log_save">
<option value="off">OFF</option>
<option value="on">ON</option>
</select>
</div>
<div class="io-row"><span class="label-inline">Max Size (MB)</span><span class="colon">:</span><input id="log_max_size" type="number" min="1" max="1000"></div>
<div class="io-row"><span class="label-inline">Max Duration (days)</span><span class="colon">:</span><input id="log_max_duration" type="number" min="1" max="365"></div>
</div>
</div>
</div>
<div class="actions">
<div class="file-upload-wrapper-device"
data-file-type="device"
data-expected-filename="deviceConfig.json"
data-upload-url="/setting/device"
data-get-url="/setting/get-device">
<input type="file" class="upload-input-device" accept=".json" style="display: none;"> <input type="file" class="upload-input-device" accept=".json" style="display: none;">
<button class="select-file-btn-device">Load Local Device File</button> <button class="select-file-btn-device">Load Local Device File</button>
</div> </div>
@ -388,8 +421,8 @@
<div class="file-upload-wrapper-protocol" <div class="file-upload-wrapper-protocol"
data-file-type="protocol" data-file-type="protocol"
data-expected-filename="protocolConfig.json" data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol" data-upload-url="/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol"> data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;"> <input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button> <button class="select-file-btn-protocol">Load Local Protocol File</button>
</div> </div>
@ -476,8 +509,8 @@
<div class="file-upload-wrapper-protocol" <div class="file-upload-wrapper-protocol"
data-file-type="protocol" data-file-type="protocol"
data-expected-filename="protocolConfig.json" data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol" data-upload-url="/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol"> data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;"> <input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button> <button class="select-file-btn-protocol">Load Local Protocol File</button>
</div> </div>
@ -557,8 +590,8 @@
<div class="file-upload-wrapper-protocol" <div class="file-upload-wrapper-protocol"
data-file-type="protocol" data-file-type="protocol"
data-expected-filename="protocolConfig.json" data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol" data-upload-url="/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol"> data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;"> <input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button> <button class="select-file-btn-protocol">Load Local Protocol File</button>
</div> </div>
@ -631,12 +664,12 @@
<div class="file-upload-wrapper-protocol" <div class="file-upload-wrapper-protocol"
data-file-type="protocol" data-file-type="protocol"
data-expected-filename="protocolConfig.json" data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol" data-upload-url="/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol"> data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;"> <input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button> <button class="select-file-btn-protocol">Load Local Protocol File</button>
</div> </div>
<button class="download-protocol-file">Download Device File</button> <button class="download-protocol-file">Download Protocol File</button>
</div> </div>
</section> </section>
@ -648,6 +681,9 @@
<!-- ───── Script ───── --> <!-- ───── Script ───── -->
<script> <script>
const API_BASE = ''; // same-origin (Nginx 프록시 사용)
const api = (p) => `${API_BASE}${p.startsWith('/') ? '' : '/'}${p}`;
const ssidList = document.getElementById('ssid-list'); const ssidList = document.getElementById('ssid-list');
const ssidTmpl = document.getElementById('ssid-template'); const ssidTmpl = document.getElementById('ssid-template');
const data = { const data = {
@ -667,11 +703,13 @@
opc_ua_server_port: null, opc_ua_server_port: null,
modbus_server_ip: '', modbus_server_ip: '',
modbus_server_port: null, modbus_server_port: null,
lte_server_ip: '',
lte_server_port: null,
/* CAN / RS-485 */ /* CAN / RS-485 */
can_bus_type: '', can_baudrate: null, can_bus_type: '', can_baudrate: null,
rs485_mode: '', rs485_baudrate: null, rs485_data_bits: null, rs485_mode: '', rs485_baudrate: null, rs485_databits: null,
rs485_parity: '', rs485_stop_bits: null, rs485_parity: '', rs485_stopbits: null,
/* IMU (축 + 부호) */ /* IMU (축 + 부호) */
imu_remap_x: '', imu_remap_y: '', imu_remap_z: '', imu_remap_x: '', imu_remap_y: '', imu_remap_z: '',
@ -679,8 +717,12 @@
/* LTE 설정 */ /* LTE 설정 */
lte_ip: '', lte_netmask: '', lte_gateway: '', lte_port: null, lte_ip: '', lte_netmask: '', lte_gateway: '', lte_port: null,
/* Log 설정 */
log_save: 'off', log_max_size: null, log_max_duration: null,
/* SSID 목록 */ /* SSID 목록 */
WIFI_SSID: [{wifi_ssid: '', wifi_password: '', wifi_security: 'none'}], WIFI_SSID: [{wifi_ssid: '', wifi_passwd: '', wifi_security: 'none'}],
}; };
const configIdMap = { const configIdMap = {
@ -694,28 +736,72 @@
'di2': 'di2', 'di2': 'di2',
'di3': 'di3', 'di3': 'di3',
'reg_can_input': 'can_input', 'reg_can_input': 'can_input',
'reg_device_type': 'device_type', 'reg_device_type': 'dev_type',
'reg_dr_on_off': 'dr_on', 'reg_dr_on_off': 'dr_on',
'reg_equipment': 'equipment', 'reg_equipment': 'equipment',
'reg_two_byte_order': 'two_byte', 'reg_two_byte_order': 'two_byte_order',
'reg_four_byte_order': 'four_byte', 'reg_four_byte_order': 'four_byte_order',
'reg_protocol': 'protocol', 'reg_protocol': 'protocol',
'reg_speed_data_source': 'speed_data_source', 'reg_speed_data_source': 'speed_data',
'reg_version': 'version', 'reg_version': 'version',
'reg_equipment_id': 'equipment_id', 'reg_equipment_id': 'equipment_id',
'reg_meid': 'meid', 'reg_meid': 'MEID',
'reg_heading_on': 'heading_on', 'reg_heading_on': 'heading_on',
'reg_heading_imu_on': 'heading_imu_on', 'reg_heading_imu_on': 'heading_imu_on',
'reg_fixmode_on': 'fixmode_on' 'reg_fixmode_on': 'fix_mode_on'
}; };
let lastLoadedRegisterData = {}; let lastLoadedRegisterData = {};
// 하이픈/공백 → 언더스코어, 대문자 통일
const normProtocol = (p) => String(p ?? '').trim().replace(/[-\s]+/g, '_');
// OPC UA 배열을 'OPC_UA' 또는 'OPC-UA' 어느 쪽이 와도 읽게
const getOpcArr = (o) => (o?.['OPC_UA'] ?? o?.['OPC-UA'] ?? []);
function normalizeRegisterForPost(reg) {
const out = JSON.parse(JSON.stringify(reg || {})); // 얕은 복제 방지
if (out.protocol) {
out.protocol = String(out.protocol).trim().toUpperCase().replace(/[-\s]+/g, '_');
}
if (out.can_input) {
out.can_input = String(out.can_input).trim().toUpperCase(); // 'on'/'off' → 'ON'/'OFF'
}
// MEID: 숫자만이면 number, 그 외는 string
if (out.MEID !== undefined && out.MEID !== null) {
const m = String(out.MEID).trim();
out.MEID = /^\d+$/.test(m) ? Number(m) : m;
}
// enum 정규화 추가
const up = s => String(s ?? '').trim().toUpperCase();
const up_us = s => up(s).replace(/[-\s]+/g, '_');
// 상위 enum들도 혹시 소문자면 대문자로
['dev_type','equipment','speed_data'].forEach(k=>{
if (out[k]) out[k] = up_us(out[k]);
});
// 빈 문자열은 전부 제거(서버 enum null 허용 대비)
Object.keys(out).forEach(k => {
if (out[k] === '') delete out[k];
});
// 업로드 파일이 'OPC-UA' 키를 쓰는 경우를 위해 보정
if (out['OPC-UA'] && !out['OPC_UA']) {
out['OPC_UA'] = out['OPC-UA'];
delete out['OPC-UA'];
}
return out;
}
const pages = { const pages = {
ssid: document.getElementById('page-ssid'), ssid: document.getElementById('page-ssid'),
network: document.getElementById('page-network'), network: document.getElementById('page-network'),
io: document.getElementById('page-io'), io: document.getElementById('page-io'),
log: document.getElementById('page-log'),
reg: document.getElementById('page-reg'), reg: document.getElementById('page-reg'),
modbus: document.getElementById('page-modbus'), modbus: document.getElementById('page-modbus'),
opcua: document.getElementById('page-opcua'), opcua: document.getElementById('page-opcua'),
@ -777,8 +863,6 @@
const jsonData = JSON.parse(DeviceFileContent); const jsonData = JSON.parse(DeviceFileContent);
loadFromData(jsonData); loadFromData(jsonData);
console.log(`Loaded ${DeviceFileType} data from local file`, jsonData);
} catch (error) { } catch (error) {
console.error(`Error uploading file (${DeviceFileType}):`, error); console.error(`Error uploading file (${DeviceFileType}):`, error);
alert(`Error uploading file: ${error.message} (${DeviceFileType})`); alert(`Error uploading file: ${error.message} (${DeviceFileType})`);
@ -830,6 +914,7 @@
try { try {
const fileContent = e.target.result; const fileContent = e.target.result;
const jsonData = JSON.parse(fileContent); const jsonData = JSON.parse(fileContent);
const payload = normalizeRegisterForPost(jsonData);
// 업로드 유효성 검사 로직 // 업로드 유효성 검사 로직
if (!isValidRegisterConfig(jsonData)){ // 예를 들어, isValidModbusConfig(jsonData) 등 if (!isValidRegisterConfig(jsonData)){ // 예를 들어, isValidModbusConfig(jsonData) 등
@ -837,9 +922,7 @@
uploadFileInput.value = ''; uploadFileInput.value = '';
return; return;
} }
loadFromRegisterJson(jsonData,fileType); loadFromRegisterJson(jsonData);
console.log(`Loaded ${fileType} data from local file (not saved).`, jsonData);
} catch (error) { } catch (error) {
alert(`Error uploading file: ${error.message} (${fileType})`); alert(`Error uploading file: ${error.message} (${fileType})`);
uploadFileInput.value = ''; uploadFileInput.value = '';
@ -854,14 +937,16 @@
/*---device 다운로드 버튼에 대한 로직---*/ /*---device 다운로드 버튼에 대한 로직---*/
document.querySelectorAll('.download-device-file').forEach(btn=>{btn.addEventListener('click', () => { document.querySelectorAll('.download-device-file').forEach(btn=>{btn.addEventListener('click', () => {
downloadDeviceJson(collectDeviceData(data)); downloadDeviceJson(collectDeviceData());
})}) })})
/*---protocol 다운로드 버튼에 대한 로직---*/ /*---protocol 다운로드 버튼에 대한 로직---*/
document.querySelectorAll('.download-protocol-file').forEach(btn=>{btn.addEventListener('click', () => { document.querySelectorAll('.download-protocol-file').forEach(btn=>{btn.addEventListener('click', () => {
downloadProtocolJson(collectProtocolData(data)); downloadProtocolJson(collectProtocolData());
})}) })})
document.getElementById('reg_can_input')?.addEventListener('change', toggleProtocolMenus); document.getElementById('reg_can_input')?.addEventListener('change', toggleProtocolMenus);
/*---modbus 테이블에 행 추가 로직---*/ /*---modbus 테이블에 행 추가 로직---*/
@ -942,7 +1027,7 @@
const canRowTemplate = document.getElementById('can-row-template'); const canRowTemplate = document.getElementById('can-row-template');
if (canRowTemplate) { if (canRowTemplate) {
const newRow = canRowTemplate.content.cloneNode(true); const newRow = canRowTemplate.content.cloneNode(true);
opcUaList.appendChild(newRow); canFieldList.appendChild(newRow);
} }
} }
@ -1175,23 +1260,23 @@
function saveToConfig() { function saveToConfig() {
const deviceData = collectDeviceData(); const deviceData = collectDeviceData();
const protocolData = collectProtocolData(); const protocolData = collectProtocolData();
const normalizedProtocol = normalizeRegisterForPost(protocolData);
if (!isValidRegisterConfig(protocolData)) { if (!isValidRegisterConfig(normalizedProtocol)) {
alert("Register settings are currently invalid. Cannot be saved."); alert("Register settings are currently invalid. Cannot be saved.");
return; return;
} }
//device //device
const deviceReq = fetch('http://localhost:8080/setting/device', { const deviceReq = fetch('/setting/device', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(deviceData) body: JSON.stringify(deviceData)
}); });
//protocol //protocol
const protocolReq = fetch('http://localhost:8080/setting/protocol', { const protocolReq = fetch('/setting/protocol', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(protocolData) body: JSON.stringify(normalizedProtocol)
}); });
// 모든 요청이 성공하면 성공 알림 // 모든 요청이 성공하면 성공 알림
@ -1275,7 +1360,7 @@
const el = document.getElementById(k); const el = document.getElementById(k);
if (el) { if (el) {
const val = el.value.trim(); const val = el.value.trim();
if (['can_baudrate', 'rs485_baudrate'].includes(k)) { if (['can_baudrate', 'rs485_baudrate', 'log_max_size', 'log_max_duration'].includes(k)) {
res[k] = val === '' ? null : Number(val); res[k] = val === '' ? null : Number(val);
} else { } else {
res[k] = val; res[k] = val;
@ -1358,7 +1443,7 @@
// OPC_UA 테이블 수집 (빈 값은 필드에서 제외) // OPC_UA 테이블 수집 (빈 값은 필드에서 제외)
const opcuaRows = document.querySelectorAll('#opcua-field-list tr'); const opcuaRows = document.querySelectorAll('#opcua-field-list tr');
collectedRegister.OPC_UA = []; collectedRegister["OPC_UA"] = [];
opcuaRows.forEach(row => { opcuaRows.forEach(row => {
const field = row.querySelector('.opcua-field-display')?.textContent.trim(); const field = row.querySelector('.opcua-field-display')?.textContent.trim();
if (field) { if (field) {
@ -1464,40 +1549,48 @@
/*-------페이지 로드시 서버에 있는 데이터를 불러오는 함수---------*/ /*-------페이지 로드시 서버에 있는 데이터를 불러오는 함수---------*/
async function loadInitialData() { async function loadInitialData() {
console.log('Starting loadInitialData...');
// 서버에서 파일 목록 조회 및 로드 (Device) // 서버에서 파일 목록 조회 및 로드 (Device)
try { try {
const deviceResponse = await fetch('http://localhost:8080/setting/get-device'); console.log('Fetching device config...');
if (deviceResponse.ok) { const deviceResponse = await fetch('/setting/get-device');
console.log('Device response status:', deviceResponse.status);
if (deviceResponse.status === 204) {
console.log('No device config in DB yet. (skip)');
} else if (deviceResponse.ok) {
const deviceData = await deviceResponse.json(); const deviceData = await deviceResponse.json();
if (deviceData) { console.log('Device data received:', deviceData);
loadFromData(deviceData); loadFromData(deviceData);
console.log('Device Load completed (Default file: deviceConfig.json)'); console.log('Device Load completed (from DB)');
} else {
console.warn('Device response received, but no body found.');
}
} else { } else {
console.warn(`Device load failed: HTTP ${deviceResponse.status}`); console.warn(`Device load failed: HTTP ${deviceResponse.status}`);
} }
} catch (error) { } catch (error) {
console.error('Device load failed:', error.message); console.error('Device load failed:', error.message, error);
} }
try { try {
const protocolResponse = await fetch('http://localhost:8080/setting/get-protocol'); console.log('Fetching protocol config...');
if (protocolResponse.ok) { const protocolResponse = await fetch('/setting/get-protocol');
console.log('Protocol response status:', protocolResponse.status);
if (protocolResponse.status === 204) {
console.log('No protocol config in DB yet. (skip)');
} else if (protocolResponse.ok) {
const protocolData = await protocolResponse.json(); const protocolData = await protocolResponse.json();
if (protocolData) { console.log('Protocol data received:', protocolData);
loadFromRegisterJson(protocolData); loadFromRegisterJson(protocolData);
console.log('Protocol Load completed (Default file: protocolConfig.json)'); console.log('Protocol Load completed (from DB)');
} else {
console.warn('Protocol response received, but no body found.');
}
} else { } else {
console.warn(`Protocol load failed: HTTP ${protocolResponse.status}`); console.warn(`Protocol load failed: HTTP ${protocolResponse.status}`);
} }
} catch (error) { } catch (error) {
console.error('./Protocol load failed:', error.message); console.error('Protocol load failed:', error.message, error);
} }
console.log('loadInitialData completed');
} }
/*-----------------------------------------------------*/ /*-----------------------------------------------------*/
@ -1505,7 +1598,7 @@
function createSsidRow(obj = {wifi_ssid: '', wifi_password: '', wifi_security: 'none'}) { function createSsidRow(obj = {wifi_ssid: '', wifi_password: '', wifi_security: 'none'}) {
const node = ssidTmpl.content.firstElementChild.cloneNode(true); const node = ssidTmpl.content.firstElementChild.cloneNode(true);
node.querySelector('.ssid-name').value = obj.wifi_ssid ?? ''; node.querySelector('.ssid-name').value = obj.wifi_ssid ?? '';
node.querySelector('.ssid-pw').value = obj.wifi_password ?? ''; node.querySelector('.ssid-pw').value = obj.wifi_passwd ?? '';
node.querySelector('select').value = (obj.wifi_security ?? 'none').toLowerCase(); node.querySelector('select').value = (obj.wifi_security ?? 'none').toLowerCase();
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
@ -1549,7 +1642,7 @@
const pw = row.querySelector('.ssid-pw').value.trim(); const pw = row.querySelector('.ssid-pw').value.trim();
const sec = row.querySelector('select').value; const sec = row.querySelector('select').value;
if (ssid === '' && pw === '') return; if (ssid === '' && pw === '') return;
if (ssid) arr.push({wifi_ssid: ssid, wifi_password: pw, wifi_security: sec}); if (ssid) arr.push({wifi_ssid: ssid, wifi_passwd: pw, wifi_security: sec});
}); });
return arr return arr
} }
@ -1562,24 +1655,27 @@
console.error("Validation Error: Data is not an object or is null."); console.error("Validation Error: Data is not an object or is null.");
return false; return false;
} }
if (typeof data.protocol === 'string') {
data.protocol = normProtocol(data.protocol);
}
// RegisterDTO의 필수 필드 (백엔드 snake_case와 일치시키세요) // RegisterDTO의 필수 필드 (백엔드 snake_case와 일치시키세요)
const topLevelRequiredFields = [ const topLevelRequiredFields = [
'protocol', 'protocol',
'analog_input_level', 'analog_input_level',
'can_input', 'can_input',
'device_type', 'dev_type',
'dr_on', 'dr_on',
'equipment', 'equipment',
'two_byte', 'two_byte_order',
'four_byte', 'four_byte_order',
'speed_data_source', 'speed_data',
'version', 'version',
'equipment_id', 'equipment_id',
'meid', 'MEID',
'heading_on', 'heading_on',
'heading_imu_on', 'heading_imu_on',
'fixmode_on' 'fix_mode_on'
]; ];
@ -1592,7 +1688,6 @@
} }
// 3. 각 필드의 값 타입 및 유효성 검증 // 3. 각 필드의 값 타입 및 유효성 검증
// protocol 필드 유효성 검사 // protocol 필드 유효성 검사
const validProtocols = ['MODBUS', 'OPC_UA', 'CAN', 'NONE']; const validProtocols = ['MODBUS', 'OPC_UA', 'CAN', 'NONE'];
if (typeof data.protocol !== 'string' || !validProtocols.includes(data.protocol)) { if (typeof data.protocol !== 'string' || !validProtocols.includes(data.protocol)) {
@ -1600,24 +1695,13 @@
return false; return false;
} }
// ON/OFF 상태를 나타내는 필드 검사
const boolLikeFields = ['dr_on', 'heading_on', 'heading_imu_on', 'fixmode_on'];
for (const field of boolLikeFields) {
if (typeof data[field] !== 'string' || (data[field] !== 'on' && data[field] !== 'off')) {
console.error(`Validation Error: Invalid value for boolean-like field '${field}'. Expected 'ON' or 'OFF', got '${data[field]}'`);
return false;
}
}
// 기타 최상위 필드의 타입 검사 (예시, 필요에 따라 추가) // 기타 최상위 필드의 타입 검사 (예시, 필요에 따라 추가)
if (typeof data.analog_input_level !== 'string' && typeof data.analog_input_level !== 'number') { if (typeof data.analog_input_level !== 'string' && typeof data.analog_input_level !== 'number') {
console.error("Validation Error: 'analog_input_level' is not a string or number."); console.error("Validation Error: 'analog_input_level' is not a string or number.");
return false; return false;
} }
// 리스트 내부 필드 유효성 검사 // 리스트 내부 필드 유효성 검사
// MODBUS 리스트 검증 // MODBUS 리스트 검증
if (data.protocol === 'MODBUS') { if (data.protocol === 'MODBUS') {
// protocol이 'MODBUS'일 경우, MODBUS 배열은 필수 // protocol이 'MODBUS'일 경우, MODBUS 배열은 필수
@ -1655,8 +1739,8 @@
// OPC_UA 리스트 검증 // OPC_UA 리스트 검증
if (data.protocol === 'OPC_UA') { if (data.protocol === 'OPC_UA') {
// protocol이 'OPC_UA'일 경우, OPC_UA 배열은 필수 const arr = getOpcArr(data);
if (!data.hasOwnProperty('OPC_UA') || !Array.isArray(data.OPC_UA)) { if (!Array.isArray(arr)) {
console.error("Validation Error: 'OPC_UA' array is required and must be an array when protocol is 'OPC_UA'."); console.error("Validation Error: 'OPC_UA' array is required and must be an array when protocol is 'OPC_UA'.");
return false; return false;
} }
@ -1675,11 +1759,6 @@
} }
} }
} else { } else {
// protocol이 'OPC_UA'가 아닌 경우, OPC_UA 배열은 존재하지 않거나 빈 배열이어야 함
if (data.hasOwnProperty('OPC_UA') && data.OPC_UA.length > 0) {
console.error("Validation Error: 'OPC_UA' array must be empty or absent when protocol is not 'OPC_UA'.");
return false;
}
// 만약 존재한다면 배열 타입이어야 함 // 만약 존재한다면 배열 타입이어야 함
if (data.hasOwnProperty('OPC_UA') && !Array.isArray(data.OPC_UA)) { if (data.hasOwnProperty('OPC_UA') && !Array.isArray(data.OPC_UA)) {
console.error("Validation Error: 'OPC_UA' field must be an array if present, even when protocol is not 'OPC_UA'."); console.error("Validation Error: 'OPC_UA' field must be an array if present, even when protocol is not 'OPC_UA'.");
@ -1687,7 +1766,6 @@
} }
} }
// CAN 리스트 검증 (can_input이 'on'인 경우 필수 - protocol 값과는 독립적) // CAN 리스트 검증 (can_input이 'on'인 경우 필수 - protocol 값과는 독립적)
if (data.can_input.toLowerCase() === 'on') { if (data.can_input.toLowerCase() === 'on') {
if (!data.hasOwnProperty('CAN') || !Array.isArray(data.CAN)) { if (!data.hasOwnProperty('CAN') || !Array.isArray(data.CAN)) {
@ -1754,8 +1832,15 @@
/*--------초기 랜더링 화면 함수------------*/ /*--------초기 랜더링 화면 함수------------*/
function loadFromData(obj) { function loadFromData(obj) {
console.log('loadFromData called with:', obj);
/* Wi-Fi 모드 radio */ /* Wi-Fi 모드 radio */
document.querySelector(`input[name="ipMode"][value="${obj.wifi_static === 'on' ? 'static' : 'dhcp'}"]`).checked = true; const radioElement = document.querySelector(`input[name="ipMode"][value="${obj.wifi_static === 'on' ? 'static' : 'dhcp'}"]`);
if (radioElement) {
radioElement.checked = true;
} else {
console.warn('Radio element not found for wifi_static:', obj.wifi_static);
}
/* 일반 input/select */ /* 일반 input/select */
for (const k in obj) { for (const k in obj) {
@ -1851,7 +1936,9 @@
// protocol 값이 있으면 select 값을 변경 (데이터 로드 시에만) // protocol 값이 있으면 select 값을 변경 (데이터 로드 시에만)
if (register.protocol && regProtocolSelect) { if (register.protocol && regProtocolSelect) {
regProtocolSelect.value = register.protocol; const p = normProtocol(register.protocol); // 'OPC-UA' → 'OPC_UA'
regProtocolSelect.value = p;
lastLoadedRegisterData.protocol = p;
} }
// 프로토콜 값에 따라 사이드바 버튼 토글 // 프로토콜 값에 따라 사이드바 버튼 토글
@ -1888,8 +1975,9 @@
const opcUaRowTemplate = document.getElementById('opcua-row-template'); const opcUaRowTemplate = document.getElementById('opcua-row-template');
if (opcUaFieldList && opcUaRowTemplate) { if (opcUaFieldList && opcUaRowTemplate) {
opcUaFieldList.innerHTML = ''; opcUaFieldList.innerHTML = '';
if (Array.isArray(register.OPC_UA) && register.OPC_UA.length > 0) { const opcArr = getOpcArr(register);
register.OPC_UA.forEach(field => { if (Array.isArray(opcArr) && opcArr.length > 0) {
opcArr.forEach(field => {
const newRow = opcUaRowTemplate.content.cloneNode(true); const newRow = opcUaRowTemplate.content.cloneNode(true);
newRow.querySelector('.opcua-field-display').textContent = field.field ?? ''; newRow.querySelector('.opcua-field-display').textContent = field.field ?? '';
newRow.querySelector('.opcua-addr-display').textContent = field.addr ?? ''; newRow.querySelector('.opcua-addr-display').textContent = field.addr ?? '';

Loading…
Cancel
Save