You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

41 KiB

NEW Web Configurator v1.5.4.2 — 시스템 아키텍처

문서 버전: 1.0 앱 버전: v1.5.4.2 (SHA fe5cd25, 태그 v1.5.4.2) 브랜치: feature/v1.5-ia-redesign (origin 푸시 완료) 작성일: 2026-06-08 상태: dev/verify 환경 .54 (192.168.55.54), 운영 .56은 v1.4.6.10 유지 상위 문서: docs/architecture.md (v2.2, v1.3.0 시대까지 커버) — 이 문서가 해당 문서를 대체함


1. 요약

NEW Web Configurator는 Telechips TCC8030 IoT 디바이스를 위한 브라우저 기반 설정 도구다. Python stdlib(표준 라이브러리) 전용 HTTP 서버 (pip 없음, 프레임워크 없음)와 바닐라 ES 모듈 SPA (빌드 단계 없음, 번들러 없음)로 구성된다. 기존 Java/Spring Boot 기반 설정 도구를 대체한 것으로, 해당 도구는 TCC8030의 700 MHz ARM에서 메모리를 436 MB 이상 점유했다 — 각 백그라운드 서비스에 48 MB MemoryMax 예산이 배정된 디바이스에서. Python 프로세스는 RSS 기준 10–30 MB 수준으로 동작한다.

핵심 설계 원칙:

  1. LAN 전용 IoT 환경 — 외부 노출 없음, 인증 없음. 모든 클라이언트는 192.168.55.x (디바이스 LAN 세그먼트) 내부에 있다. CORS는 설계상 전체 개방.
  2. SQLite 공유 소유권 — 세 개의 독립 프로그램(이 서비스, 레거시 Java app-runner, dpworldapp 데이터 처리 바이너리)이 동일한 ~/db/dynamic_data.db를 읽고 쓴다. 키 수준 소유권 정책(board_config_key_ownership_policy)이 주된 안전 메커니즘이다.
  3. config-reader 계약 준수device_configprotocol_config는 디바이스 config-reader 계약과 정확히 일치해야 한다. 이를 통해 레거시 Java 앱이 .56에서 읽고 쓰는 동작이 예기치 않게 깨지지 않도록 보장한다. 자동화된 드리프트 감지가 이를 강제한다.
  4. Partial-merge(부분 병합)만 허용 — 전체 행 REPLACE 없음. DBManager.update_config (BEGIN IMMEDIATE RMW)가 모든 POST에서 미설정 키를 보존하도록 보장한다.
  5. v1.5.x IA 재설계 — 사이드바 중첩 5그룹 네비게이션, Firmware OTA 실제 통합, Lucide SVG 아이콘, Inter/JetBrains Mono 셀프 호스팅 폰트, gzip + ETag/304 성능 최적화, behavioral(jsdom) 테스트 인프라.

2. 하드웨어 컨텍스트 (Telechips TCC8030)

항목
CPU 700 MHz ARM (싱글코어 주, 멀티코어 가능)
OS Yocto/Poky 4.0.17 (uname -r: Telechips)
펌웨어 업데이트 A/B 파티션 슬롯 (slot A / slot B, dpw-fw-update-tool 경유)
영속 파티션 /home/root/db (mmcblk0p11) — SQLite DB; /opt/log (mmcblk0p12) — dpworldapp 로그
임시 파티션 /tmp — tmpfs, 서비스별 격리 슬라이스 PrivateTmp=yes
/etc/systemd/system tmpfs — 여기에 작성된 유닛 파일은 재부팅 시 소실됨
systemd 유닛 위치 /lib/systemd/system/web-configurator.service (영속, v1.5.2 H17 수정)
MemoryMax 48 MB (서비스 유닛에 설정)
Web Configurator 포트 9090 (직접 연결, 앞단 nginx 없음)
Java app-runner 포트 8080 (.56에서 nginx :80 → Java :8080)
WiFi 칩셋 Qualcomm QCA6490 (PCI 17CB:1103, 드라이버 cnss_pci)
펌웨어 바이너리 /lib/firmware/amss.bin (WiFi FW 버전은 QC_IMAGE_VERSION_STRING=으로 추출)
dpworldapp 바이너리 /usr/bin/dpworldapp — CAN/Modbus/OPC-UA 데이터 처리기
GNSS u-blox GPS (버전은 dpworldapp 로그 [GNSS FW VER : ...]에서 확인)
앱 로그 경로 /opt/log/dpworldapp/dpworldapp_YYYY-MM-DD.log

tmpfs 인시던트 (2026-05-29): Poky 4.0.17에서 /etc/systemd/system/은 tmpfs다. v1.4.0 배포 시 유닛을 /etc/에 작성했고, 다음 재부팅 전까지는 유지되었다. v1.5.2 H17에서 deploy.ps1이 /lib/systemd/system/(영속)에 쓰도록 수정했다.

ReadWritePaths: systemd 유닛은 ProtectSystem=strict를 사용한다. 서비스가 쓰기 접근해야 하는 경로는 ReadWritePaths에 명시적으로 나열해야 한다:

  • /opt/log/dpworldapp — 로그 다운로드 임시 부모 경로 (v1.4.6.6에서 /tmp로 수정)
  • /opt/fw_staging — 펌웨어 스테이징 디렉토리 (v1.5.2 C1)
  • /opt/config_backups — 플래시 전 설정 백업 (v1.5.2 C1)
  • /home/root/db — SQLite DB 경로

3. 기술 스택

백엔드

컴포넌트 기술
HTTP 서버 Python 3.10+ http.server.BaseHTTPRequestHandler + socketserver.ThreadingMixIn
데이터베이스 sqlite3 stdlib
압축 gzip stdlib (응답 압축)
설정 os.environ + 상수
의존성 Python stdlib 전용 (pip 없음, 서드파티 패키지 없음)
진입점 src/server.py:main()

프론트엔드

컴포넌트 기술
JS 모듈 시스템 바닐라 ES 모듈 (type="module", 번들러 없음)
아이콘 Lucide v0.460+ SVG 인라인 (src/static/js/icons.js, 49개 항목)
타이포그래피 Inter (5 웨이트) + JetBrains Mono (2 웨이트) — .woff2 셀프 호스팅 (src/static/fonts/)
스타일시트 단일 src/static/css/style.css (~3000줄 이상)
XSS 이스케이프 src/static/js/utils.jsescapeHtml (5문자 명시 치환: & < > " ')

테스트

컴포넌트 기술
Python 테스트 pytest (v1.5.4.2 기준 644개 테스트)
JS behavioral 테스트 Node 18+ node:test + jsdom (4개 .mjs 파일에 35개 테스트)
듀얼 러너 python -m pytest + npm test
디바이스 배포 격리 deploy.ps1git archive HEAD src 사용 — tests/node_modules/package.json은 배포 대상 아님

배포

  • PowerShell scripts/deploy.ps1 — git archive → scp → tar → atomic mv → systemctl restart
  • 헬스 체크 + 실패 시 자동 롤백 (v1.5.2 H18)
  • /lib/systemd/system/ 유닛 설치 (v1.5.2 H17 영속화)

4. 프로세스 모델

systemd web-configurator.service
    └── Python process (server.py main())
          ├── ThreadedHTTPServer (ThreadingMixIn + HTTPServer)
          │     └── ConfigHandler per thread (BaseHTTPRequestHandler)
          ├── Log auto-compress daemon thread (5-min interval)
          └── _recover_staging background daemon thread (firmware, v1.5.2 M10/M15)
  • socket.setdefaulttimeout(30.0) — 글로벌 slowloris 방어 (v1.5.1 H4). 비활성 상태 30초 후 모든 연결이 타임아웃된다.
  • /api/firmware/upload 라우트는 대용량 펌웨어 파일 업로드 완료를 허용하기 위해 연결별로 connection.timeout = None으로 재설정한다 (v1.5.2 H20).
  • ThreadedHTTPServer.timeout = 30.0 — 서버 수준 accept 타임아웃.
  • 서버 프로세스당 DBManager 인스턴스 하나 (server.py:89의 모듈 수준 db = DBManager()). 각 public 메서드는 SQLite 연결을 건드리기 전에 self._lock (threading.Lock)을 획득한다.

5. 데이터 레이어

데이터베이스 파일

/home/root/db/dynamic_data.db   (mmcblk0p11, 영속 파티션)

테이블

테이블 소유자 목적
board_config 공유 (Python + Java + dpworldapp + Super_Relay) 키-값 설정 저장소
event_history dpworldapp 이벤트 큐 — Web Configurator는 쓰기 안 함
schema_meta Python Web Configurator 마이그레이션 플래그 (멱등 시작)
sqlite_sequence SQLite 내부 AUTOINCREMENT 추적
board_config_audit Python Web Configurator 설정 변경 감사 로그

board_config 키 — 소유권

소유자 Java 접근 여부 dpworldapp 읽기 여부 비고
device_config Python + Java 공유 예, 전체 행 REPLACE device config-reader 계약 적용 — 37개 필드
protocol_config Python 주 예 (.56 구버전) device register-mapping 계약 — ~21개 필드
log_config Python 전용 아니오 아니오 Web Configurator 로그 관리 — v1.0.2
security_config Super_Relay 프로젝트 아니오 아니오 외부 프로젝트 — 건드리지 말 것
transport_config Super_Relay 프로젝트 아니오 아니오 외부 프로젝트 — 건드리지 말 것

핵심 원칙: Python 전용 설정은 반드시 별도 키에 넣어야 한다(device_config/protocol_config 금지). Java의 전체 행 REPLACE가 모든 Java POST 시 Python 전용 필드를 조용히 삭제하기 때문이다.

DBManager 백엔드 (db_manager.py)

우선순위 체인:

  1. pythonsqlite3 stdlib 모듈 (권장; 운영 환경에서 사용)
  2. cli — subprocess 경유 sqlite3 CLI (import 실패 시 폴백)
  3. json — JSON 파일 폴백 (테스트/긴급 용도만; dpworldapp 호환 없음)

Atomic RMW (db_manager.py:226 update_config):

  • python 백엔드: 단일 연결 내에서 BEGIN IMMEDIATE + SELECT + INSERT OR REPLACE + COMMIT 수행. 동시 쓰기 차단. 지수 백오프로 잠금 재시도 (3회, 100 ms 기본).
  • cli + json 백엔드: self._lock으로 직렬화.
  • 결과: N개 동시 스레드 간 업데이트 유실 없음 (10스레드 + 20스레드 동시 증분 테스트 통과 확인 — tests/test_v1_5_3_multi_instance.py).

마이그레이션 (src/migrations.py)

시작 시 apply_all_migrations(db) (server.py:728)를 통해 적용됨. Fail-soft: (OperationalError, DatabaseError, JSONDecodeError, ValueError) 예외를 잡아서 처리.

마이그레이션 schema_meta 키 수행 내용
migrate_can_baudrate_units can_baudrate_units_v2 구 baudrate 값 2500/5000/10000 → 250/500/1000으로 재매핑
migrate_log_compress_split log_compress_split_v1 log_compress_size_mb/log_compress_age_daysdevice_configlog_config로 이동
migrate_port_types port_types_int_v1 문자열 타입 서버 포트를 Integer로 정규화

6. 요청 라우팅 (server.py)

do_GET 라우트

경로 핸들러 설명
/setting/get-device _handle_get_device device_configlog_config 병합 반환 (log 필드 삽입)
/setting/get-protocol _handle_get_protocol protocol_config 반환
/setting/log-files _handle_get_log_files dpworldapp 로그 파일 목록
/setting/kernel-bundle _handle_get_kernel_bundle journalctl + 커널 로그 .tar.gz (v1.1.0)
/setting/log-stats _handle_get_log_stats 로그 디스크 사용량 통계
/api/mac _handle_get_mac WiFi MAC 주소 (디바이스 식별)
/api/health _handle_health 서버 헬스 (uptime, DB 백엔드)
/api/system-status _handle_system_status 홈 대시보드: core_app / communication / network / system / hardware_modules / dpworldapp_status
/api/support-bundle _handle_support_bundle 진단 ZIP 다운로드
/api/firmware/status _FW.status() Firmware OTA 상태 스냅샷 (v1.5.0 P4a)
정적 파일 _serve_static_file Realpath 경로 순회 방어 + gzip + ETag + Cache-Control

do_POST 라우트

경로 핸들러 설명
/setting/device _handle_post_device device config partial-merge (atomic RMW)
/setting/protocol _handle_post_protocol protocol config partial-merge + process_protocol_config
/setting/log-download _handle_post_log_download 선택한 로그 파일 → .tar.gz
/api/test-connections _handle_test_connections 설정된 서버 엔드포인트 TCP 프로브
/api/restart-dpworldapp _handle_restart_dpworldapp systemctl restart dpworldapp
/api/firmware/upload _FW.upload() 펌웨어 ZIP을 스테이징 디렉토리로 스트리밍
/api/firmware/preflight _FW.preflight() 플래시 전 사전 점검
/api/firmware/flash _FW.flash() 8단계 OTA 플래시 실행
/api/firmware/restore-check _FW.restore_check() 재부팅 후 설정 무결성 검사

라우팅 가드

  • /setting/ 접두사: 알 수 없는 경로 → 404 (SPA로 폴스루 없음)
  • /api/ 접두사: 알 수 없는 경로 → 404 (v1.5.0.1 수정, server.py do_GET)
  • 정적 파일: os.realpath() + 접두사 검사 (경로 순회 차단, server.py:237-239)
  • SPA 폴백: 나머지 모든 GET 경로에 index.html 반환

정적 파일 서빙 (v1.5.4)

# server.py:224 _serve_static_file()

ETag = W/"mtime-size"              # weak ETag (저비용, 해시 없음)
If-None-Match == ETag  304        # 0바이트 body

Cache-Control:
  text/html      no-cache, must-revalidate
  JS/CSS         no-cache, must-revalidate   (v1.5.4.2 H1+M2: 즉시 배포 전파)
  font/image     public, max-age=86400

gzip:
  Accept-Encoding q값 파싱 (v1.5.4.2 L1)
  압축 대상: text/*, application/javascript, application/json, image/svg+xml
  제외: .woff2, 이미지, 256바이트 미만
  레벨: 6, Vary: Accept-Encoding 헤더 추가

Body 크기 제한: MAX_BODY_SIZE = 1 MB (펌웨어 업로드는 예외 — 소켓 타임아웃 None).


7. 프론트엔드 아키텍처 (v1.5.0 IA 재설계)

진입점

src/static/index.html — lang="en", title="IoT Web Configurator", 9개 modulepreload 힌트 (app / state / api / utils / icons / constants / page-dirty / nav-guard / home).

사이드바 레이아웃 (5그룹, 11개 활성 리프)

sidebar (aside[role=navigation])
├── Dashboard
│   └── home
├── Network
│   ├── wifi          (Wi-Fi SSID + 연결 + 지역)
│   ├── ethernet      (이더넷 IP + LTE 인터페이스)
│   └── server-setting (TIOT/Update/RTCM/LTE 서버 엔드포인트)
├── Interface & Protocol
│   ├── general-settings  (Equipment + Protocol 선택 + Odometer)
│   ├── sensor-io         (RS485 + Analog/Digital 포트)
│   ├── can-bus           (CAN 설정 + 프레임 매핑)
│   ├── opcua             (OPC UA 엔드포인트 + 필드 매핑)
│   └── modbus            (Modbus 엔드포인트 + 바이트 순서 + 필드 매핑)
├── Log
│   └── log               (3탭: Configuration / Files / Kernel)
└── Firmware
    └── firmware          (8단계 OTA 스테퍼)

그룹 펼침 상태는 localStorage("wc.sidebar.groups")에 유지된다.

app.js PAGES에 레거시 별칭 보존 (구 북마크 하위 호환):

  • ssidwifi
  • ioethernet
  • networkserver-setting
  • registergeneral (= general-settings 페이지)
  • cancan-bus

페이지 객체 패턴

모든 페이지 모듈은 기본 페이지 객체를 export한다:

export default {
    render(container) { ... },   // HTML을 container에 주입
    mount(container)  { ... },   // 이벤트 리스너 연결, collector 등록
    destroy()         { ... },   // 리스너 정리 (선택)
    validate()        { ... },   // 클라이언트 측 유효성 검사 (선택)
}

구현 대상: app.js, state.js, 11개 리프 페이지 전체.

상태 관리 (state.js)

  • state.device — 평탄화된 device config 객체 (JS 인메모리)
  • state.protocol — 평탄화된 protocol config 객체
  • state.pageDirty — 페이지별 dirty 매트릭스 (10개 항목): {home, wifi, ethernet, server-setting, general, sensor-io, can-bus, opcua, modbus, log, firmware: false}
  • state.isDirty — 불리언 하위 호환 별칭 (모든 pageDirty 값의 OR)
  • convertFlatToNested(flat) / convertNestedToFlat(nested) — 레거시 앱의 평탄화 표현과 일치하는 중첩 ↔ 평탄 키 변환

Page Dirty + Nav Guard

src/static/js/page-dirty.js:

  • markDirty(pageId) / clearDirty(pageId) / hasDirty() / getDirtyPages() / clearAllDirty()
  • 사이드바 점 (.nav-item__dirty)은 모든 호출 시 자동 동기화.

src/static/js/nav-guard.js:

  • confirmNavigation(currentPageId) → Promise 모달 (Save / Discard / Cancel)
  • 포커스 트랩 (모달 내부 Tab/Shift+Tab 순환) — v1.5.2 H12 + H13
  • Esc 키 = Cancel
  • 닫을 때 트리거 포커스 복원 (WCAG 2.4.3) — v1.5.2 H13

아이콘 (icons.js)

  • 49개 Lucide v0.460+ SVG body 문자열 (ISC 라이선스)
  • icon(name, opts) 헬퍼: opts.cls (클래스, <>"' 제거), opts.aria (aria-label, escapeAttr)
  • escapeAttr는 모듈 수준에서 정의 (성능 최적화, 호출별 생성 아님)
  • 모든 아이콘에 role="img" + <title> 적용 (WCAG 4.1.2)
  • 장식용 아이콘에는 aria-hidden="true" 적용

보안 (프론트엔드)

  • utils.jsescapeHtml: & → &amp; / < → &lt; / > → &gt; / " → &quot; / ' → &#39; — 5문자 명시 치환 (v1.5.2 H1 루트 XSS 수정)
  • 운영자 입력값(레지스터 테이블, odometer 필드, import 데이터) 모두 DOM 삽입 전 escapeHtml 통과 — v1.4.6.2 F1 + C5 + C6
  • icons.js icon()의 cls/aria: escapeAttr<>"' 제거
  • nav-guard.js _escapeescapeHtml을 미러링
  • DEBUG console.log: state.device 내용 마스킹 (값 대신 키만 표시) — v1.4.6.9 M6

8. Firmware OTA (v1.5.0 Phase 4a + v1.5.2 하드닝)

소스

C:\Users\F1304\Downloads\dpw-fw-update-tool\webconfig_fw\에서 MERGE.md + DESIGN.md (2026-06-06)에 따라 병합.

백엔드 패키지 (src/firmware/)

모듈 라인 수 역할
fw_client.py ~110 dpw-fw-update-tool 데몬용 Wire 클라이언트 (host:port)
staging.py ~47 ZIP 스테이징: 압축 해제, 유효성 검사, 슬롯 감지
config_safety.py ~172 플래시 전 설정 백업, 재부팅 후 복원 검증
protocol.py ~64 Wire 프로토콜 저수준 프레이밍
fw_controller.py ~480 상태 기반 OTA 오케스트레이션, 상태 스키마, 컴포넌트별 진행 바, 재부팅 watchdog
fw_routes.py ~110 전송 계층 독립적 HTTP 라우트 핸들러, RouteError 예외 타입

server.py:94에서 초기화:

_FW = FirmwareRoutes(FirmwareController(
    fw_host=os.environ.get("FW_HOST", "127.0.0.1"),
    fw_port=int(os.environ.get("FW_PORT", "8990")),
    staging_dir=os.environ.get("FW_STAGING_DIR", "/opt/fw_staging"),
    backups_dir=os.environ.get("FW_BACKUPS_DIR", "/opt/config_backups"),
    db_path=DB_PATH,
))

8단계 스테퍼

Upload → Verify → Pre-flight → Backup → Flash → Commit → Reboot → Config check

상태 스키마 (UI의 단일 진실 공급원)

{
  "state": "idle|staged|flashing|rebooting|verifying|done|failed",
  "phases": [{ "key", "label", "state": "pending|active|done|error", "tier", "detail" }],
  "components": [{ "role", "signature", "name", "size", "sha256",
                   "sent", "pct", "state": "pending|active|done|error" }],
  "overall": { "sent", "total", "pct", "elapsed_s" },
  "active_signature": "RTF|null",
  "backup": { "path", "when" } ,
  "slot": { "before": "A|B|?", "after": "A|B|?|null" },
  "message": "human line",
  "error": null
}

v1.5.2 Firmware 하드닝

항목 수정 내용
C1 ProtectSystem 서비스 유닛 + deploy.ps1에 /opt/fw_staging + /opt/config_backupsReadWritePaths 추가, 디렉토리 사전 생성
H4 stage_zip TOCTOU fw_controller.stage_zipself._lock 하에서 'staging' 상태 검사
H19 default_file FirmwareController default_file 오버라이드를 server.py에서 제거
H20 upload timeout /api/firmware/upload 소켓 타임아웃을 연결별로 None으로 재설정
M10/M15 recover _recover_staging을 백그라운드 데몬 스레드로 실행 (시작 비차단)

9. 보안 모델

보안 모델은 레거시 Java 앱 기준선에 맞춰 인증 없는 LAN 전용으로 명시적으로 설계되었다.

제어 항목 구현 방식
인증 없음 의도적 설계 (LAN 전용 IoT, Java 앱과 동일)
CORS Access-Control-Allow-Origin: * (전체 origin 허용, 의도적)
HTTP만 사용 TLS 미적용 (LAN 환경)
XSS — 출력 utils.jsescapeHtml 5문자 치환 (v1.5.2 H1 루트 수정)
XSS — 속성 icons.jsescapeAttr, 모든 숫자 속성에 숫자 강제 변환 Number() (v1.4.6.9 C5/C6)
경로 순회 os.realpath() + STATIC_DIR 접두사 검사 (server.py:237-239)
SQL 인젝션 DBManager.ALLOWED_KEYS 화이트리스트 (db_manager.py:42) — CLI 백엔드는 키를 파라미터화
Body 크기 MAX_BODY_SIZE = 1 MB (server.py:161)
Slowloris socket.setdefaulttimeout(30.0) (server.py, v1.5.1)
enum 인젝션 유효하지 않은 enum 값 시 HTTP 400 하드 거부 (config_validator.py)
Super_Relay 키 POST에 security_config/transport_config 포함 시 HTTP 400 거부 (v1.4.4)
systemd 하드닝 NoNewPrivileges, ProtectSystem=strict, ProtectHome=read-only, PrivateTmp, ReadWritePaths 화이트리스트
지원 번들 WiFi 비밀번호는 포함 전 마스킹 처리

10. 성능 (v1.5.4 + v1.5.4.2)

정적 파일 전달 (LAN, .54 측정값)

지표
최초 로드 전송량 ~575 KB → ~200 KB (gzip으로 ~65% 감소)
반복 방문 ETag/304, 0바이트 body — 거의 즉각 렌더링
배포 전파 즉시 (JS/CSS no-cache, v1.5.4.2 H1+M2 수정)
정적 파일 레이턴시 20–32 ms (LAN 왕복)
system-status 콜드 ~6.09 s (journalctl + dpworldapp 로그 스캔)
system-status 웜 ~1.15 s (모듈 캐시)

적용된 기법

  • gzip: 레벨 6, text/JS/CSS/JSON/SVG, ≥256B, q값 인식 Accept-Encoding (v1.5.4.2 L1)
  • ETag: W/"mtime-size" 약한 ETag, 파일별, If-None-Match → 304
  • Cache-Control: HTML/JS/CSS no-cache, must-revalidate; 폰트/이미지 max-age=86400
  • modulepreload: 9개 핵심 모듈 (index.html 링크 힌트) — app.js 파싱 전 병렬 fetch
  • 셀프 호스팅 폰트: 7개 .woff2 파일 (Inter 5웨이트 + JetBrains Mono 2웨이트, latin 서브셋, 총 ~200 KB) — 외부 CDN 의존 없음 (LAN 전용 IoT에서 필수, v1.5.1.1)
  • amss.bin에 mmap 사용: 전체 읽기 대신 mmap.find() — 피크 RSS 1 MB 미만 유지 (v1.4.0)

11. 동시성 모델

Thread A (request)      Thread B (request)      Daemon (auto-compress)
       │                        │                        │
   _lock.acquire()          blocks                   _lock.acquire() (after A)
   BEGIN IMMEDIATE ──────── SQLite WAL ────────── (reads log_config)
   SELECT + mutator()
   INSERT OR REPLACE
   COMMIT
   _lock.release() ─────── unblocks B
  • DBManager._lockthreading.Lock — 하나의 Python 객체, 프로세스 로컬.
  • SQLite 측의 BEGIN IMMEDIATE는 DB 레벨에서 동시 쓰기를 차단한다 (WAL 모드는 동시 읽기 허용).
  • fw_controller 상태 머신은 별도 인스턴스의 self._lock으로 보호.
  • Nav-guard 이중 모달 레이스: 빠른 클릭 시 시각적 문제만 발생 (기록됨, 허용 수준).

12. 테스트 인프라 (v1.5.3 듀얼 러너)

Python pytest (644개 테스트)

tests/에 버전/기능별로 구성:

패턴 설명
Grep 테스트 코드 패턴 존재/부재 검증 (대다수 테스트)
Behavioral 테스트 server.py 통합 (FakeDB), config_validator 하드 거부
Multi-instance test_v1_5_3_multi_instance.py — DBManager BEGIN IMMEDIATE 레이스 (10+20 스레드)
Schema drift test_schema_drift.pyscripts/audit_schema_drift.pytests/fixtures/java_schema_expected.json

실행: python -m pytest tests/ -v

Node 18+ jsdom (35개 테스트, 4개 파일)

파일 테스트 수 검증 내용
tests/test_nav_guard_behavior.mjs 9 포커스 트랩 Tab/Shift+Tab, Esc, Save/Discard/Cancel, pageId XSS
tests/test_icons_svg.mjs 10 SVG 정합성, viewBox/width/height, aria role=img + title, XSS 저항성
tests/test_log_tabs_switching.mjs 4 탭 클릭 → panel.hidden 토글, aria-selected, 기본 상태
tests/test_save_all_modal.mjs 9 모달 role/aria, dirty 페이지 목록, Cancel/Confirm promise, escapeHtml XSS
tests/test_firmware_logic.mjs 3 (기존) firmware-logic.js 헬퍼

실행: npm test (Node 18+ 필요, 최초 1회 npm install)

디바이스 배포 격리: deploy.ps1git archive HEAD src 사용 — src/만 배포됨. tests/, node_modules/, package.json, package-lock.json은 디바이스로 전송되지 않는다.


13. 배포 파이프라인 (scripts/deploy.ps1)

단계

1. git archive HEAD src → 로컬 tar
2. SSH: mkdir -p $AppDir /opt/fw_staging /opt/config_backups
3. scp tar → 디바이스
4. SSH: tar --warning=no-timestamp -xf tar → _deploy_tmp/
5. SSH: 기존 src/ 백업 → backups/src-$ts (최근 3개 유지)
6. SSH: atomic mv _deploy_tmp/ → src/
7. SSH: /lib/systemd/system/web-configurator.service 설치 (영속)
8. SSH: systemctl daemon-reload + enable + restart web-configurator
9. Confirm-Health: /api/health 최대 30초 폴링
10. 실패 시 자동 롤백: backups/src-$ts 복원 + restart

주요 플래그

플래그 효과
(기본값) 클린 git 워킹 트리 + 버전 태그 필요
-AllowUntagged 태그 요건 생략 (dev/test 배포)
-Rollback 가장 최근 backup/src-* 복원

배포 후 디바이스 상태

  • $AppDirDEPLOYED_VERSION 파일 (버전 문자열)
  • $AppDirdeploy-history.log (배포마다 타임스탬프 + 버전)
  • /lib/systemd/system/web-configurator.service의 systemd 유닛

버전 확인 (배포 후)

ssh root@192.168.55.54 "curl -s http://localhost:9090/api/health"
# → {"status":"ok","version":"v1.5.4.2","uptime_s":...}

14. 운영 리뷰 커버리지

v1.5.2 멀티에이전트 리뷰 (wf_f4786747-995)

  • 12개 차원, 318개 에이전트, 약 102건 제기 → 41건 확정
  • 수정: Critical 1 + High 20 + Medium 7 = 28개 항목
  • 이연: High 5 (behavioral, jsdom) → v1.5.3
  • 방법: 발견 사항별 3-lens 적대 검증 (정확성 / 보안 / 재현성) — 다수 반박 (2/3) = false positive, 폐기

v1.5.4.2 집중 리뷰 (wf_a4efa2e3-2cb)

  • 6개 차원, 21건 제기 → 5건 확정
  • 수정: H1+M2 Cache-Control JS/CSS, M1 sensor-io placeholder, L1 gzip q-value, L2 focus-visible
  • 잔여: Medium 9 + Low 4 (사용자 체감 영향 0)

15. v1.5.x 릴리즈 타임라인

태그 커밋 핵심 변경 테스트 수
v1.5.0-phase-1 c6ec698 인프라: 사이드바 5그룹 + icons.js + page-dirty + nav-guard + Firmware placeholder 481
v1.5.0-phase-2 1effda8 Network 3-리프: wifi + ethernet + server-setting 500
v1.5.0-phase-3 6bdc2a7 Interface & Protocol 5-리프: general + sensor-io + can-bus + opcua + modbus 523
v1.5.0-phase-4a (sha) Firmware OTA 실제 병합 (webconfig_fw → src/firmware/ + 5개 라우트 + firmware.js 902줄) 557
v1.5.0 b3776a1 Phase 4b: 이모지 → Lucide SVG ~80개 인스턴스 + log 3탭 + Save All 확인 모달 562
v1.5.0.1 2b74ad2 핫픽스: deploy.ps1 최초 배포 mkdir + /api/ 404 가드 564
v1.5.1 1c3ffc2 DBManager.update_config BEGIN IMMEDIATE RMW + slowloris 30초 타임아웃 569
v1.5.1.1 73e188d Inter + JetBrains Mono .woff2 7개 폰트 셀프 호스팅 572
v1.5.2 6b7047d 운영 리뷰 번들: 41건 확정 수정 (C1 ProtectSystem + H1 XSS + H17 systemd + H18 rollback + ...) 599
v1.5.3 3b04e36 Behavioral 테스트 인프라: jsdom + node:test, 4개 .mjs 파일, 35개 Node 테스트 Python 615 + Node 35
v1.5.3.1 (sha) 핫픽스: analog_input_level enum 5V/10V/20mA → 2/4/6 (Java 회귀) + sensor-io UI 카드 621
v1.5.3.2 (sha) 핫픽스: modbus.js two_byte_order 누락 (Java TwoByte 회귀) 625
v1.5.4 (sha) 성능: gzip + ETag/304 + Cache-Control + modulepreload (13개 테스트) 638
v1.5.4.1 (sha) 핫픽스: 잔여 이모지 → SVG (1개 테스트) 639
v1.5.4.2 fe5cd25 핫픽스 집중 리뷰: Cache-Control no-cache JS/CSS + sensor-io placeholder + gzip q-value + focus-visible (5개 테스트) 644

16. 컴포넌트 다이어그램 (ASCII)

최상위 시스템 구성

  Operator Browser (LAN)
       │
       ├──:9090 (direct)──► Python web-configurator.service
       │                          │
       │                     server.py (ThreadedHTTPServer)
       │                          │
       │                    ┌─────┼──────────────────────────────┐
       │                    │     │                              │
       │              DBManager  firmware/              system_status.py
       │              (sqlite3)  fw_controller.py        kernel_log.py
       │                    │     │                      log_manager.py
       │                    │  /opt/fw_staging           support_bundle.py
       │                    │  /opt/config_backups       config_validator.py
       │                    │                              dpworldapp_enums.py
       │                    │                              enum_normalizer.py
       │               ~/db/dynamic_data.db               migrations.py
       │                    │
       │           ┌────────┼────────────┐
       │      board_config  schema_meta  event_history
       │           │
       │    ┌──────┼──────────┐
       │  device_ protocol_  log_
       │  config   config    config
       │
       ├──:80──► nginx ──:8080──► Java app-runner (legacy, .56 only)
       │                               │
       │                        ~/db/dynamic_data.db (same file!)
       │
       └── (no browser)──► dpworldapp (/usr/bin/dpworldapp)
                                   │
                            ~/db/dynamic_data.db (reads device_config + protocol_config)
                            /opt/log/dpworldapp/*.log (writes)

프론트엔드 모듈 그래프

index.html
    └── app.js (type=module, entry point)
          ├── state.js          (device/protocol 상태 + flat↔nested)
          ├── api.js            (모든 REST 엔드포인트 fetch 래퍼)
          ├── utils.js          (escapeHtml, debounce)
          ├── icons.js          (49개 Lucide SVG + icon() 헬퍼)
          ├── constants.js      (APP_VERSION, DEFAULTS, enum 집합)
          ├── page-dirty.js     (markDirty/clearDirty/사이드바 점)
          ├── nav-guard.js      (confirmNavigation 모달 + 포커스 트랩)
          ├── toast.js          (토스트 알림)
          ├── validator.js      (클라이언트 측 필드 유효성 검사)
          ├── components/
          │   ├── crud-table.js  (레지스터 필드 매핑 테이블)
          │   └── ip-input.js    (IP 주소 입력 위젯)
          └── pages/
              ├── home.js             (Dashboard)
              ├── wifi.js             (Wi-Fi 설정)
              ├── ethernet.js         (이더넷 + LTE 인터페이스)
              ├── server-setting.js   (서버 엔드포인트)
              ├── general-settings.js (Equipment + Protocol + Odometer)
              ├── sensor-io.js        (RS485 + Analog/Digital 포트)
              ├── can-bus.js          (CAN 설정 + 프레임 매핑)
              ├── opcua.js            (OPC UA 엔드포인트 + 매핑)
              ├── modbus.js           (Modbus 엔드포인트 + 바이트 순서 + 매핑)
              ├── log.js              (로그 관리 3탭)
              └── firmware.js         (Firmware OTA 8단계 스테퍼)
              [legacy aliases: ssid.js / io.js / network.js / register.js / can.js]

Partial-Merge 데이터 흐름 (POST /setting/device)

Browser POST {partial JSON}
       │
  server.py:_handle_post_device
       │
  _read_request_body()       ← 1 MB 제한; dict 가드 (배열/스칼라 거부)
       │
  normalize_device_input()   ← enum_normalizer.py (case alias: ON→on 등)
       │
  validate_wifi_country_code()  ← 유효하지 않은 2자 코드 시 하드 거부 400
  validate_rs485_integers()     ← bool/string 타입 시 하드 거부 400
  validate_device_port_types_hard()  ← 유효하지 않은 포트 값 시 하드 거부 400
  validate_super_relay_keys()   ← security/transport_config 포함 시 하드 거부 400
       │
  db.update_config("device_config", mutator)
       │
       ├── BEGIN IMMEDIATE (SQLite 쓰기 잠금)
       ├── SELECT 기존 데이터
       ├── 기존 데이터 + POST body 병합 (partial-merge)
       ├── log_config 키 분리 → db.update_config("log_config", ...)
       ├── INSERT OR REPLACE device_config
       └── COMMIT
       │
  응답: {success, merged_keys, preserved_keys_count}

17. 운영 런북

.54 배포 (dev/verify)

cd C:\Development\NEW_Web_Configurator
.\scripts\deploy.ps1 192.168.55.54 -AllowUntagged

.54 태그 배포 (릴리즈)

git tag v1.5.4.2 -m "v1.5.4.2"
.\scripts\deploy.ps1 192.168.55.54

롤백

.\scripts\deploy.ps1 192.168.55.54 -Rollback

수동 서비스 재시작 (deploy.ps1 restart가 조용히 실패할 때)

ssh root@192.168.55.54 "systemctl restart web-configurator"

배포 헬스 확인

ssh root@192.168.55.54 "curl -s http://localhost:9090/api/health"
# 예상 결과: {"status":"ok","version":"v1.5.4.2",...}

DB 점검

ssh root@192.168.55.54 "sqlite3 /home/root/db/dynamic_data.db 'SELECT key, length(value) FROM board_config;'"
# schema_meta 마이그레이션 점검
ssh root@192.168.55.54 "sqlite3 /home/root/db/dynamic_data.db 'SELECT key, value, applied_at FROM schema_meta;'"

펌웨어 업로드 (OTA)

curl -F "file=@firmware.zip" http://192.168.55.54:9090/api/firmware/upload
curl -s http://192.168.55.54:9090/api/firmware/preflight
curl -X POST http://192.168.55.54:9090/api/firmware/flash

스키마 드리프트 확인 (로컬)

python scripts/audit_schema_drift.py
# Exit 0 = 정상, 2 = 드리프트 감지, 1 = 오류

전체 테스트 실행

python -m pytest tests/ -v      # Python: 644개 테스트
npm test                         # Node: 35개 jsdom 테스트

18. 알려진 제한 사항 및 백로그

항목 상태 비고
system-status 콜드 레이턴시 ~6초 v1.5.5 후보 모든 콜드 호출 시 journalctl + 전체 로그 스캔
Java 측 wipe 경로 미해결 (Java는 수정 불가) 레거시 앱이 알 수 없는 enum 값을 조용히 null 처리하고, null 필드를 쓰기 시 삭제 — Python 하드 거부가 1차 방어선
main 브랜치 FF 머지 보류 사용자 결정 사항 .56 운영 환경은 명시적 사용자 승인 전까지 v1.4.6.10 유지
GNSS 스캐너 최신 로그만 처리 낮은 우선순위 로그 회전 후 dpworldapp 재시작 시 GNSS 티어가 'na' 표시 — 다음 dpworldapp 시작 시 자가 복구
collectAllPagesData 비대칭 v1.5.5 후보 Device Save → 3행 쓰기, Register Save → 1행 — 구조적 재설계 필요
리뷰에서 Medium 9 + Low 4 이연 사용자 체감 영향 0 — 기각 또는 v1.5.5+ 처리
16개 페이지 동적 import v1.6+ 후보 현재 모든 페이지 즉시 로드; 동적 import 적용 시 초기 파싱 감소 가능

부록 A — 파일 트리 (src/)

src/
├── server.py               ← HTTP 서버, 라우팅, 진입점
├── db_manager.py           ← SQLite 3티어 + update_config atomic RMW
├── config_validator.py     ← 서버 측 유효성 검사 + 정규화
├── dpworldapp_enums.py     ← device 계약 enum 값 (단일 진실 공급원)
├── enum_normalizer.py      ← Case-only alias 정규화 (ON→on 등)
├── migrations.py           ← 시작 마이그레이션 (3개 적용)
├── log_manager.py          ← 로그 목록/다운로드/압축/삭제/통계 + auto-compress 데몬
├── kernel_log.py           ← journalctl + 커널 로그 번들 빌더
├── system_status.py        ← 홈 대시보드 상태 집계
├── support_bundle.py       ← 진단 ZIP 생성기
├── firmware/
│   ├── __init__.py
│   ├── fw_client.py        ← dpw-fw-update-tool 데몬용 Wire 클라이언트
│   ├── staging.py          ← ZIP 스테이징 및 유효성 검사
│   ├── config_safety.py    ← 설정 백업/복원
│   ├── protocol.py         ← Wire 프로토콜 프레이밍
│   ├── fw_controller.py    ← 상태 기반 OTA 오케스트레이션
│   └── fw_routes.py        ← HTTP 라우트 핸들러
└── static/
    ├── index.html          ← SPA 진입점, 5그룹 사이드바, modulepreload 힌트
    ├── css/style.css       ← 디자인 토큰 + 컴포넌트 스타일 + firmware CSS
    ├── fonts/              ← Inter (.woff2 ×5) + JetBrains Mono (.woff2 ×2)
    ├── img/                ← dp-world-logo.svg
    └── js/
        ├── app.js          ← 라우터, Save All, Import/Export, 사이드바 JS
        ├── api.js          ← REST 클라이언트 (모든 fetch 래퍼)
        ├── state.js        ← 인메모리 상태 + flat↔nested 변환
        ├── page-dirty.js   ← 페이지별 dirty 매트릭스 + 사이드바 점
        ├── nav-guard.js    ← 네비게이션 확인 모달 + 포커스 트랩
        ├── icons.js        ← Lucide v0.460+ 49개 SVG + icon() 헬퍼
        ├── constants.js    ← APP_VERSION + DEFAULTS + enum 집합
        ├── utils.js        ← escapeHtml + 헬퍼
        ├── toast.js        ← 토스트 알림
        ├── validator.js    ← 클라이언트 측 필드 유효성 검사
        ├── country-codes.js ← Wi-Fi 국가 코드 205개 드롭다운용
        ├── firmware-logic.js ← Firmware OTA 순수 로직 헬퍼
        ├── components/
        │   ├── crud-table.js
        │   └── ip-input.js
        └── pages/
            ├── home.js             (활성)
            ├── wifi.js             (활성)
            ├── ethernet.js         (활성)
            ├── server-setting.js   (활성)
            ├── general-settings.js (활성)
            ├── sensor-io.js        (활성)
            ├── can-bus.js          (활성)
            ├── opcua.js            (활성)
            ├── modbus.js           (활성)
            ├── log.js              (활성)
            ├── firmware.js         (활성)
            ├── ssid.js             (레거시 별칭 → wifi)
            ├── io.js               (레거시 별칭 → ethernet, RS485/CAN 잔재)
            ├── network.js          (레거시 별칭 → server-setting)
            ├── register.js         (레거시 별칭 → general-settings)
            └── can.js              (레거시 별칭 → can-bus)

부록 B — 주요 코드 위치

내용 파일 라인/함수
진입점 / main() src/server.py main() (파일 하단)
HTTP 핸들러 클래스 src/server.py class ConfigHandler
정적 파일 서빙 src/server.py _serve_static_file() ~L224
gzip + ETag 로직 src/server.py _serve_static_file() ~L254-299
Partial-merge POST device src/server.py _handle_post_device()
Partial-merge POST protocol src/server.py _handle_post_protocol()
Firmware 라우트 src/server.py do_GET + do_POST firmware 분기
Atomic RMW src/db_manager.py update_config() L226
BEGIN IMMEDIATE src/db_manager.py update_config() L258
Java enum 값 src/dpworldapp_enums.py DEVICE_ENUM_VALUES L16, PROTOCOL_ENUM_VALUES L39
Case alias 정규화 src/enum_normalizer.py normalize_device_input(), normalize_protocol_input()
마이그레이션 src/migrations.py apply_all_migrations()
escapeHtml (5문자) src/static/js/utils.js escapeHtml()
icon() 헬퍼 src/static/js/icons.js icon()
page-dirty 매트릭스 src/static/js/state.js state.pageDirty
nav-guard 모달 src/static/js/nav-guard.js confirmNavigation()
flat↔nested 변환 src/static/js/state.js convertFlatToNested(), convertNestedToFlat()
FirmwareController src/firmware/fw_controller.py class FirmwareController
Schema drift 테스트 tests/test_schema_drift.py 파일 전체
Config 계약 스냅샷 tests/fixtures/java_schema_expected.json 파일 전체
감사 스크립트 scripts/audit_schema_drift.py 파일 전체
jsdom nav-guard 테스트 tests/test_nav_guard_behavior.mjs 파일 전체

부록 C — 관련 문서

문서 위치 내용
구 아키텍처 (v2.2) docs/architecture.md v1.3.0 시대, 현재 이 문서로 대체됨
배포 런북 docs/DEPLOY.md 배포 절차, 버전 관리
2026-05-29 인시던트 내부 인시던트 기록(이 배포물에 미포함) 전체 행 replace wipe 인시던트
변경 이력 CHANGELOG.md v1.4.0 → v1.5.4.2 전체 릴리즈 이력