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. 182
      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. 270
      src/main/resources/static/index.html

12
build.gradle

@ -26,12 +26,14 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
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'
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.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
@SpringBootApplication
// (exclude = {
// DataSourceAutoConfiguration.class,
// HibernateJpaAutoConfiguration.class
//})
public class MobidigimProjectApplication {
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;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
@ -21,7 +18,10 @@ public class ObjectMapperConfig {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
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;
}

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")
public ResponseEntity<DeviceDTO> getDevice() throws IOException {
DeviceDTO entity = settingService.readFileWithDevice();
return ResponseEntity.ok(entity);
if (!settingService.deviceFileExists()) { // DB에 없음
return ResponseEntity.noContent().build(); // 204
}
return ResponseEntity.ok(settingService.readFileWithDevice());
}
@PostMapping("/protocol")
@ -51,8 +53,10 @@ public class Controller {
@GetMapping("/get-protocol")
public ResponseEntity<RegisterDTO> getRegister() throws IOException {
RegisterDTO entity = settingService.readFileWithRegister();
return ResponseEntity.ok(entity);
if (!settingService.protocolFileExists()) { // DB에 없음
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
private Lte lteConfig;
@JsonUnwrapped
private Log logConfig;
@JsonProperty("WIFI_SSID")
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;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
@ -14,4 +15,22 @@ public enum Rs485Parity {
public String getValue() {
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;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import org.mobidgim.mobidigimproject.model.device.enums.Rs485Parity;
import org.mobidgim.mobidigimproject.model.device.enums.Rs485Baudrate;
import org.mobidgim.mobidigimproject.model.device.enums.Rs485Mode;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
@ -16,9 +21,11 @@ public class Rs485 {
private Rs485Baudrate rs485Baudrate;
private Integer rs485DataBits;
private Integer rs485Databits;
@Enumerated(EnumType.STRING)
@JsonProperty("rs485_parity")
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 String modbusServerIp;
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")
private String wifiSsid;
@JsonProperty("wifi_password")
private String wifiPassword;
@JsonProperty("wifi_passwd")
private String wifiPasswd;
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;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter;
import lombok.Setter;
import org.mobidgim.mobidigimproject.model.register.enums.*;
@ -36,38 +38,51 @@ public class RegisterDTO {
private AnalogPort di3;
@JsonProperty("can_input")
private CanInput canInput;
private DeviceType deviceType;
@JsonProperty("dev_type")
private DeviceType devType;
@JsonProperty("dr_on")
private DR drOn;
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 SpeedDataSource speedDataSource;
@JsonProperty("speed_data")
private SpeedDataSource speedData;
private String version;
@JsonProperty("equipment_id")
private String equipmentId;
@JsonProperty("MEID")
@JsonDeserialize(using = MeidDeserializer.class)
private String meid;
@JsonProperty("heading_on")
private HeadingOn headingOn;
@JsonProperty("heading_imu_on")
private HeadingImuOn headingImuOn;
private FixmodeOn fixmodeOn;
@JsonProperty("fix_mode_on")
private FixmodeOn fixModeOn;
@JsonProperty("MODBUS")
private List<ModbusField> MODBUS;
@JsonProperty("OPC_UA")
@JsonAlias({"OPC_UA","OPC-UA"})
private List<OpcUaField> OPC_UA;
@JsonProperty("CAN")

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

@ -7,11 +7,11 @@ import lombok.AllArgsConstructor;
public enum DeviceType {
TIOT("TIOT"),
RTLS("RTLS");
private final String Device;
private final String Dev;
@JsonValue
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;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
@ -15,4 +16,17 @@ public enum FourByte {
public String getFourByte() {
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;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
@ -15,4 +16,21 @@ public enum OutputDataType {
public String getOdt() {
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;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
@ -15,4 +16,12 @@ public enum Protocol {
public String getProtocol() {
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;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
@ -13,4 +14,14 @@ public enum TwoByte {
public String getTwoByte() {
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> {
}

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

@ -1,193 +1,145 @@
package org.mobidgim.mobidigimproject.service;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
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.register.RegisterDTO;
import org.mobidgim.mobidigimproject.model.register.enums.CanInput;
import org.mobidgim.mobidigimproject.model.register.enums.Protocol;
import org.mobidgim.mobidigimproject.model.register.sebDTO.CanField;
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.mobidgim.mobidigimproject.repository.BoardConfigRepository;
import org.springframework.stereotype.Service;
import javax.swing.text.html.parser.Parser;
import java.io.File;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class SettingService {
private final BoardConfigRepository boardConfigRepository;
private final ObjectMapper mapper;
private static final String DEFAULT_DEVICE_FILENAME = "deviceConfig.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
public SettingService(ObjectMapper mapper) {
this.mapper = mapper;
this.mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
@PostConstruct
void initMapper() {
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
// 디렉토리 생성 메서드
private void ensureDirectoryExists() throws IOException {
Path dirPath = Paths.get(TEMP_DIRECTORY);
if (!Files.exists(dirPath)) {
Files.createDirectory(dirPath);
}
}
/* ===================== Device ===================== */
// Device 파일 저장 (기본 파일명 사용)
public void saveFileWithDevice(DeviceDTO request) throws IOException {
saveFileWithDevice(request, DEFAULT_DEVICE_FILENAME);
}
// Device 파일 저장 (사용자 지정 파일명)
public void saveFileWithDevice(DeviceDTO request, String filename) throws IOException {
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
try {
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
ensureDirectoryExists();
String filePath = TEMP_DIRECTORY + "/" + filename;
Files.write(Paths.get(filePath), json.getBytes(StandardCharsets.UTF_8));
BoardConfig row = new BoardConfig();
row.setKey(KEY_DEVICE);
row.setValue(json);
boardConfigRepository.save(row); // upsert (PK=`key`)
} catch (Exception e) {
throw new IOException("Failed to save device_config to DB", e);
}
}
// Device 파일 읽기 (기본 파일명 사용)
public DeviceDTO readFileWithDevice() throws IOException {
return readFileWithDevice(DEFAULT_DEVICE_FILENAME);
}
// Device 파일 읽기 (사용자 지정 파일명)
public DeviceDTO readFileWithDevice(String filename) throws IOException {
String filePath = TEMP_DIRECTORY + "/" + filename;
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new IOException("Device configuration file not found: " + filename);
try {
return boardConfigRepository.findById(KEY_DEVICE)
.map(bc -> {
try { return mapper.readValue(bc.getValue(), DeviceDTO.class); }
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() {
return deviceFileExists(DEFAULT_DEVICE_FILENAME);
}
public boolean deviceFileExists(String filename) {
String filePath = TEMP_DIRECTORY + "/" + filename;
return Files.exists(Paths.get(filePath));
return boardConfigRepository.existsById(KEY_DEVICE);
}
// Protocol 파일 저장 (기본 파일명 사용)
/* ===================== Protocol ===================== */
public RegisterDTO saveFileWithRegister(RegisterDTO request) throws IOException {
return saveFileWithRegister(request, DEFAULT_PROTOCOL_FILENAME);
}
// Protocol 파일 저장 (사용자 지정 파일명)
public RegisterDTO saveFileWithRegister(RegisterDTO request, String filename) throws IOException {
// 프로토콜에 따른 데이터 정리
if (request.getProtocol() != Protocol.OPC_UA) {
request.setOPC_UA(null);
}
if (request.getProtocol() != Protocol.MODBUS) {
request.setMODBUS(null);
}
if (request.getCanInput() != CanInput.ON) {
request.setCAN(null);
}
// 기존 정리 로직 유지
if (request.getProtocol() != Protocol.OPC_UA) 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);
ensureDirectoryExists();
String filePath = TEMP_DIRECTORY + "/" + filename;
Path path = Paths.get(filePath);
Files.write(path, json.getBytes(StandardCharsets.UTF_8));
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);
}
}
// Protocol 파일 읽기 (기본 파일명 사용)
public RegisterDTO readFileWithRegister() throws IOException {
return readFileWithRegister(DEFAULT_PROTOCOL_FILENAME);
}
// Protocol 파일 읽기 (사용자 지정 파일명)
public RegisterDTO readFileWithRegister(String filename) throws IOException {
String filePath = TEMP_DIRECTORY + "/" + filename;
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
throw new IOException("Protocol configuration file not found: " + filename);
try {
return boardConfigRepository.findById(KEY_PROTOCOL)
.map(bc -> {
try { return mapper.readValue(bc.getValue(), RegisterDTO.class); }
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() {
return protocolFileExists(DEFAULT_PROTOCOL_FILENAME);
}
public boolean protocolFileExists(String filename) {
String filePath = TEMP_DIRECTORY + "/" + filename;
return Files.exists(Paths.get(filePath));
return boardConfigRepository.existsById(KEY_PROTOCOL);
}
// temp 디렉토리의 모든 JSON 파일 목록 조회
/* ===================== 목록 대체 ===================== */
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 {
return getAvailableFiles("protocol");
}
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());
return boardConfigRepository.existsById(KEY_PROTOCOL)
? Collections.singletonList(DEFAULT_PROTOCOL_FILENAME)
: Collections.emptyList();
}
}

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

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

@ -16,6 +16,7 @@
<button data-page="ssid">SSID</button>
<button data-page="network">Network &amp; Server</button>
<button data-page="io">I/O</button>
<button data-page="log">Log</button>
</div>
</details>
<details class="nav">
@ -60,8 +61,8 @@
<div class="file-upload-wrapper-device"
data-file-type="device"
data-expected-filename="deviceConfig.json"
data-upload-url="http://localhost:8080/setting/device"
data-get-url="http://localhost:8080/setting/get-device">
data-upload-url="/setting/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>
@ -98,17 +99,19 @@
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
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>
<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>
<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 class="actions">
<div class="file-upload-wrapper-device"
data-file-type="device"
data-expected-filename="deviceConfig.json"
data-upload-url="http://localhost:8080/setting/device"
data-get-url="http://localhost:8080/setting/get-device">
data-upload-url="/setting/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>
@ -169,14 +172,14 @@
</select></div>
<div class="io-row"><span class="label-inline">Parity</span><span
class="colon">:</span><select id="rs485_parity">
<option value="">NONE</option>
<option value="no">NONE</option>
<option value="odd">ODD</option>
<option value="even">EVEN</option>
</select></div>
<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
class="colon">:</span><input id="rs485_stop_bits"></div>
class="colon">:</span><input id="rs485_stopbits"></div>
</div>
</div>
<!-- CAN -->
@ -239,8 +242,38 @@
<div class="file-upload-wrapper-device"
data-file-type="device"
data-expected-filename="deviceConfig.json"
data-upload-url="http://localhost:8080/setting/device"
data-get-url="http://localhost:8080/setting/get-device">
data-upload-url="/setting/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;">
<button class="select-file-btn-device">Load Local Device File</button>
</div>
@ -388,8 +421,8 @@
<div class="file-upload-wrapper-protocol"
data-file-type="protocol"
data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol">
data-upload-url="/setting/protocol"
data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button>
</div>
@ -476,8 +509,8 @@
<div class="file-upload-wrapper-protocol"
data-file-type="protocol"
data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol">
data-upload-url="/setting/protocol"
data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button>
</div>
@ -557,8 +590,8 @@
<div class="file-upload-wrapper-protocol"
data-file-type="protocol"
data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol">
data-upload-url="/setting/protocol"
data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button>
</div>
@ -631,12 +664,12 @@
<div class="file-upload-wrapper-protocol"
data-file-type="protocol"
data-expected-filename="protocolConfig.json"
data-upload-url="http://localhost:8080/setting/protocol"
data-get-url="http://localhost:8080/setting/get-protocol">
data-upload-url="/setting/protocol"
data-get-url="/setting/get-protocol">
<input type="file" class="upload-input-protocol" accept=".json" style="display: none;">
<button class="select-file-btn-protocol">Load Local Protocol File</button>
</div>
<button class="download-protocol-file">Download Device File</button>
<button class="download-protocol-file">Download Protocol File</button>
</div>
</section>
@ -648,6 +681,9 @@
<!-- ───── Script ───── -->
<script>
const API_BASE = ''; // same-origin (Nginx 프록시 사용)
const api = (p) => `${API_BASE}${p.startsWith('/') ? '' : '/'}${p}`;
const ssidList = document.getElementById('ssid-list');
const ssidTmpl = document.getElementById('ssid-template');
const data = {
@ -667,11 +703,13 @@
opc_ua_server_port: null,
modbus_server_ip: '',
modbus_server_port: null,
lte_server_ip: '',
lte_server_port: null,
/* CAN / RS-485 */
can_bus_type: '', can_baudrate: null,
rs485_mode: '', rs485_baudrate: null, rs485_data_bits: null,
rs485_parity: '', rs485_stop_bits: null,
rs485_mode: '', rs485_baudrate: null, rs485_databits: null,
rs485_parity: '', rs485_stopbits: null,
/* IMU (축 + 부호) */
imu_remap_x: '', imu_remap_y: '', imu_remap_z: '',
@ -679,8 +717,12 @@
/* LTE 설정 */
lte_ip: '', lte_netmask: '', lte_gateway: '', lte_port: null,
/* Log 설정 */
log_save: 'off', log_max_size: null, log_max_duration: null,
/* SSID 목록 */
WIFI_SSID: [{wifi_ssid: '', wifi_password: '', wifi_security: 'none'}],
WIFI_SSID: [{wifi_ssid: '', wifi_passwd: '', wifi_security: 'none'}],
};
const configIdMap = {
@ -694,28 +736,72 @@
'di2': 'di2',
'di3': 'di3',
'reg_can_input': 'can_input',
'reg_device_type': 'device_type',
'reg_device_type': 'dev_type',
'reg_dr_on_off': 'dr_on',
'reg_equipment': 'equipment',
'reg_two_byte_order': 'two_byte',
'reg_four_byte_order': 'four_byte',
'reg_two_byte_order': 'two_byte_order',
'reg_four_byte_order': 'four_byte_order',
'reg_protocol': 'protocol',
'reg_speed_data_source': 'speed_data_source',
'reg_speed_data_source': 'speed_data',
'reg_version': 'version',
'reg_equipment_id': 'equipment_id',
'reg_meid': 'meid',
'reg_meid': 'MEID',
'reg_heading_on': 'heading_on',
'reg_heading_imu_on': 'heading_imu_on',
'reg_fixmode_on': 'fixmode_on'
'reg_fixmode_on': 'fix_mode_on'
};
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 = {
ssid: document.getElementById('page-ssid'),
network: document.getElementById('page-network'),
io: document.getElementById('page-io'),
log: document.getElementById('page-log'),
reg: document.getElementById('page-reg'),
modbus: document.getElementById('page-modbus'),
opcua: document.getElementById('page-opcua'),
@ -777,8 +863,6 @@
const jsonData = JSON.parse(DeviceFileContent);
loadFromData(jsonData);
console.log(`Loaded ${DeviceFileType} data from local file`, jsonData);
} catch (error) {
console.error(`Error uploading file (${DeviceFileType}):`, error);
alert(`Error uploading file: ${error.message} (${DeviceFileType})`);
@ -830,6 +914,7 @@
try {
const fileContent = e.target.result;
const jsonData = JSON.parse(fileContent);
const payload = normalizeRegisterForPost(jsonData);
// 업로드 유효성 검사 로직
if (!isValidRegisterConfig(jsonData)){ // 예를 들어, isValidModbusConfig(jsonData) 등
@ -837,9 +922,7 @@
uploadFileInput.value = '';
return;
}
loadFromRegisterJson(jsonData,fileType);
console.log(`Loaded ${fileType} data from local file (not saved).`, jsonData);
loadFromRegisterJson(jsonData);
} catch (error) {
alert(`Error uploading file: ${error.message} (${fileType})`);
uploadFileInput.value = '';
@ -854,14 +937,16 @@
/*---device 다운로드 버튼에 대한 로직---*/
document.querySelectorAll('.download-device-file').forEach(btn=>{btn.addEventListener('click', () => {
downloadDeviceJson(collectDeviceData(data));
downloadDeviceJson(collectDeviceData());
})})
/*---protocol 다운로드 버튼에 대한 로직---*/
document.querySelectorAll('.download-protocol-file').forEach(btn=>{btn.addEventListener('click', () => {
downloadProtocolJson(collectProtocolData(data));
downloadProtocolJson(collectProtocolData());
})})
document.getElementById('reg_can_input')?.addEventListener('change', toggleProtocolMenus);
/*---modbus 테이블에 행 추가 로직---*/
@ -942,7 +1027,7 @@
const canRowTemplate = document.getElementById('can-row-template');
if (canRowTemplate) {
const newRow = canRowTemplate.content.cloneNode(true);
opcUaList.appendChild(newRow);
canFieldList.appendChild(newRow);
}
}
@ -1175,23 +1260,23 @@
function saveToConfig() {
const deviceData = collectDeviceData();
const protocolData = collectProtocolData();
if (!isValidRegisterConfig(protocolData)) {
const normalizedProtocol = normalizeRegisterForPost(protocolData);
if (!isValidRegisterConfig(normalizedProtocol)) {
alert("Register settings are currently invalid. Cannot be saved.");
return;
}
//device
const deviceReq = fetch('http://localhost:8080/setting/device', {
const deviceReq = fetch('/setting/device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(deviceData)
});
//protocol
const protocolReq = fetch('http://localhost:8080/setting/protocol', {
const protocolReq = fetch('/setting/protocol', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(protocolData)
body: JSON.stringify(normalizedProtocol)
});
// 모든 요청이 성공하면 성공 알림
@ -1275,7 +1360,7 @@
const el = document.getElementById(k);
if (el) {
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);
} else {
res[k] = val;
@ -1358,7 +1443,7 @@
// OPC_UA 테이블 수집 (빈 값은 필드에서 제외)
const opcuaRows = document.querySelectorAll('#opcua-field-list tr');
collectedRegister.OPC_UA = [];
collectedRegister["OPC_UA"] = [];
opcuaRows.forEach(row => {
const field = row.querySelector('.opcua-field-display')?.textContent.trim();
if (field) {
@ -1464,40 +1549,48 @@
/*-------페이지 로드시 서버에 있는 데이터를 불러오는 함수---------*/
async function loadInitialData() {
console.log('Starting loadInitialData...');
// 서버에서 파일 목록 조회 및 로드 (Device)
try {
const deviceResponse = await fetch('http://localhost:8080/setting/get-device');
if (deviceResponse.ok) {
console.log('Fetching device config...');
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();
if (deviceData) {
console.log('Device data received:', deviceData);
loadFromData(deviceData);
console.log('Device Load completed (Default file: deviceConfig.json)');
} else {
console.warn('Device response received, but no body found.');
}
console.log('Device Load completed (from DB)');
} else {
console.warn(`Device load failed: HTTP ${deviceResponse.status}`);
}
} catch (error) {
console.error('Device load failed:', error.message);
console.error('Device load failed:', error.message, error);
}
try {
const protocolResponse = await fetch('http://localhost:8080/setting/get-protocol');
if (protocolResponse.ok) {
console.log('Fetching protocol config...');
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();
if (protocolData) {
console.log('Protocol data received:', protocolData);
loadFromRegisterJson(protocolData);
console.log('Protocol Load completed (Default file: protocolConfig.json)');
} else {
console.warn('Protocol response received, but no body found.');
}
console.log('Protocol Load completed (from DB)');
} else {
console.warn(`Protocol load failed: HTTP ${protocolResponse.status}`);
}
} 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'}) {
const node = ssidTmpl.content.firstElementChild.cloneNode(true);
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();
const deleteBtn = document.createElement('button');
@ -1549,7 +1642,7 @@
const pw = row.querySelector('.ssid-pw').value.trim();
const sec = row.querySelector('select').value;
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
}
@ -1562,24 +1655,27 @@
console.error("Validation Error: Data is not an object or is null.");
return false;
}
if (typeof data.protocol === 'string') {
data.protocol = normProtocol(data.protocol);
}
// RegisterDTO의 필수 필드 (백엔드 snake_case와 일치시키세요)
const topLevelRequiredFields = [
'protocol',
'analog_input_level',
'can_input',
'device_type',
'dev_type',
'dr_on',
'equipment',
'two_byte',
'four_byte',
'speed_data_source',
'two_byte_order',
'four_byte_order',
'speed_data',
'version',
'equipment_id',
'meid',
'MEID',
'heading_on',
'heading_imu_on',
'fixmode_on'
'fix_mode_on'
];
@ -1592,7 +1688,6 @@
}
// 3. 각 필드의 값 타입 및 유효성 검증
// protocol 필드 유효성 검사
const validProtocols = ['MODBUS', 'OPC_UA', 'CAN', 'NONE'];
if (typeof data.protocol !== 'string' || !validProtocols.includes(data.protocol)) {
@ -1600,24 +1695,13 @@
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') {
console.error("Validation Error: 'analog_input_level' is not a string or number.");
return false;
}
// 리스트 내부 필드 유효성 검사
// MODBUS 리스트 검증
if (data.protocol === 'MODBUS') {
// protocol이 'MODBUS'일 경우, MODBUS 배열은 필수
@ -1655,8 +1739,8 @@
// OPC_UA 리스트 검증
if (data.protocol === 'OPC_UA') {
// protocol이 'OPC_UA'일 경우, OPC_UA 배열은 필수
if (!data.hasOwnProperty('OPC_UA') || !Array.isArray(data.OPC_UA)) {
const arr = getOpcArr(data);
if (!Array.isArray(arr)) {
console.error("Validation Error: 'OPC_UA' array is required and must be an array when protocol is 'OPC_UA'.");
return false;
}
@ -1675,11 +1759,6 @@
}
}
} 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)) {
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 값과는 독립적)
if (data.can_input.toLowerCase() === 'on') {
if (!data.hasOwnProperty('CAN') || !Array.isArray(data.CAN)) {
@ -1754,8 +1832,15 @@
/*--------초기 랜더링 화면 함수------------*/
function loadFromData(obj) {
console.log('loadFromData called with:', obj);
/* 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 */
for (const k in obj) {
@ -1851,7 +1936,9 @@
// protocol 값이 있으면 select 값을 변경 (데이터 로드 시에만)
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');
if (opcUaFieldList && opcUaRowTemplate) {
opcUaFieldList.innerHTML = '';
if (Array.isArray(register.OPC_UA) && register.OPC_UA.length > 0) {
register.OPC_UA.forEach(field => {
const opcArr = getOpcArr(register);
if (Array.isArray(opcArr) && opcArr.length > 0) {
opcArr.forEach(field => {
const newRow = opcUaRowTemplate.content.cloneNode(true);
newRow.querySelector('.opcua-field-display').textContent = field.field ?? '';
newRow.querySelector('.opcua-addr-display').textContent = field.addr ?? '';

Loading…
Cancel
Save