diff --git a/README.md b/README.md index e69de29..b4bed1c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,203 @@ +# ESP32 Web Flasher + +브라우저에서 ESP32 계열 마이컴에 펌웨어를 플래시하는 Docker 기반 웹 애플리케이션입니다. +별도 소프트웨어 설치 없이 Chrome/Edge 브라우저와 USB 케이블만으로 펌웨어를 구워 넣을 수 있습니다. + +## 주요 특징 + +- **Web Serial API** — 드라이버·CLI 도구 없이 브라우저 직접 시리얼 통신 +- **esp-web-tools v10** (Espressif 공식) — 안정적인 웹 컴포넌트 기반 플래싱 +- **다중 바이너리 지원** — bootloader / partition table / application 개별 또는 병합 바이너리 +- **펌웨어 관리** — 업로드·목록·삭제, UUID 기반 REST API +- **실시간 로그** — 색상 구분 시리얼 모니터 내장 +- **Docker Compose** — 단일 명령으로 백엔드(Node.js) + 프론트엔드(Nginx) 기동 + +## 지원 칩 + +| 칩 패밀리 | 지원 여부 | +|----------|---------| +| ESP32-S3 | ✓ 권장 (내장 USB-OTG 지원) | +| ESP32-S2 | ✓ | +| ESP32-C3 | ✓ | +| ESP32 | ✓ | + +## 브라우저 호환성 + +| 브라우저 | 지원 | +|--------|------| +| Chrome 89+ | ✓ | +| Edge 89+ | ✓ | +| Firefox | ✗ (Web Serial 미지원) | +| Safari | ✗ (Web Serial 미지원) | + +> **참고:** HTTP(localhost 제외)에서는 Web Serial API가 동작하지 않습니다. 외부 배포 시 HTTPS가 필수입니다. + +## 빠른 시작 + +### 사전 요구사항 + +- Docker Engine 20.10+ +- Docker Compose v2+ +- Chrome 89+ 또는 Edge 89+ + +### 실행 + +```bash +git clone https://gitea.byunc.com/byun/webflash.git +cd webflash +docker compose up -d --build +``` + +브라우저에서 `http://localhost:3100` 접속 + +### 중지 + +```bash +docker compose down +``` + +펌웨어 데이터까지 삭제하려면: + +```bash +docker compose down -v +``` + +## 사용 방법 + +### 1단계 — 시리얼 연결 확인 + +1. ESP32를 USB 케이블로 PC에 연결 +2. **포트 연결** 버튼 클릭 → 브라우저 포트 선택 대화상자에서 포트 선택 +3. VID/PID가 표시되고 Espressif(0x303A) 장치 여부 확인 + +### 2단계 — 펌웨어 업로드 + +**펌웨어 업로드** 탭에서: + +- 이름, 버전, 칩 패밀리 입력 +- `.bin` 파일 드래그&드롭 또는 클릭하여 선택 + - 병합 바이너리(merged.bin): 펌웨어 하나만 업로드 + - 분리 파일: bootloader(0x0000) + partitions(0x8000) + app(0x10000) +- **서버에 업로드** 클릭 + +### 3단계 — 플래시 + +**플래시** 탭에서: + +1. 목록에서 펌웨어 선택 +2. **ESP32S3 플래시 실행** 버튼 클릭 +3. esp-web-tools 대화상자에서 포트 선택 후 플래시 진행 + +## 플래시 파일 준비 + +### Arduino IDE에서 내보내기 + +``` +Sketch → Export Compiled Binary +→ 스케치 폴더에 .bin 파일 생성 +``` + +### 병합 바이너리 생성 (권장) + +```bash +esptool.py --chip esp32s3 merge_bin \ + -o merged.bin \ + 0x0 bootloader.bin \ + 0x8000 partitions.bin \ + 0x10000 app.bin +``` + +### PlatformIO 사용 시 + +```bash +pio run # 개별 바이너리 빌드 +pio run --target mergebin # 병합 바이너리 생성 +``` + +## 프로젝트 구조 + +``` +webflash/ +├── backend/ +│ ├── Dockerfile # Node.js 20 Alpine 이미지 +│ ├── package.json +│ └── server.js # Express REST API 서버 +├── frontend/ +│ ├── Dockerfile # Nginx Alpine 이미지 +│ ├── nginx.conf # 리버스 프록시 설정 +│ ├── index.html # 단일 페이지 앱 (한국어 UI) +│ ├── css/style.css # 다크 테마 스타일 +│ └── js/app.js # Web Serial API 로직 +└── docker-compose.yml +``` + +## REST API + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| `GET` | `/api/health` | 헬스 체크 | +| `GET` | `/api/firmware` | 펌웨어 목록 조회 | +| `POST` | `/api/firmware/upload` | 펌웨어 업로드 | +| `GET` | `/api/firmware/:id/manifest` | esp-web-tools 매니페스트 생성 | +| `DELETE` | `/api/firmware/:id` | 펌웨어 삭제 | +| `GET` | `/firmware/files/:filename` | 바이너리 파일 서빙 | + +### 업로드 요청 예시 + +```bash +curl -X POST http://localhost:3100/api/firmware/upload \ + -F "name=My Firmware" \ + -F "version=1.0.0" \ + -F "chipFamily=ESP32-S3" \ + -F "firmware=@app.bin" \ + -F "bootloader=@bootloader.bin" \ + -F "partitions=@partitions.bin" +``` + +### 매니페스트 응답 예시 + +```json +{ + "name": "My Firmware", + "version": "1.0.0", + "new_install_prompt_erase": true, + "builds": [{ + "chipFamily": "ESP32-S3", + "parts": [ + { "path": "/firmware/files/xxxx-bootloader.bin", "offset": 0 }, + { "path": "/firmware/files/xxxx-partitions.bin", "offset": 32768 }, + { "path": "/firmware/files/xxxx-app.bin", "offset": 65536 } + ] + }] +} +``` + +## 환경 변수 + +| 변수 | 기본값 | 설명 | +|------|--------|------| +| `PORT` | `3000` | 백엔드 포트 | +| `NODE_ENV` | `production` | 실행 환경 | +| `ALLOWED_ORIGIN` | `*` | CORS 허용 오리진 | + +## 기술 스택 + +| 영역 | 기술 | +|------|------| +| 백엔드 | Node.js 20, Express 4, Multer, UUID | +| 프론트엔드 | Vanilla JS, HTML5, CSS3 | +| 웹 서버 | Nginx Alpine (리버스 프록시) | +| 플래싱 | esp-web-tools v10, Web Serial API | +| 컨테이너 | Docker, Docker Compose | +| 저장소 | 파일 시스템 + JSON 메타데이터 | + +## 보안 고려사항 + +- 파일 업로드는 `.bin` 확장자 및 8 MB 크기 제한 +- XSS 방지를 위한 HTML 이스케이프 처리 +- 외부 배포 시 HTTPS 적용 필수 +- ESP32 Flash Encryption 활성화 권장 (펌웨어 덤프 방지) + +## 라이선스 + +MIT diff --git a/report.html b/report.html new file mode 100644 index 0000000..cb40050 --- /dev/null +++ b/report.html @@ -0,0 +1,1067 @@ + + + + + + ESP32 Web Flasher — 기술 분석 보고서 + + + + + +
+ +

ESP32 Web Flasher

+

+ 브라우저 기반 ESP32 펌웨어 플래시 시스템
+ Docker 컨테이너 아키텍처 기술 분석 보고서 +

+
+
+
작성일
+
2025년 5월
+
+
+
버전
+
v1.0.0
+
+
+
분류
+
기술 분석
+
+
+
저장소
+
gitea.byunc.com/byun/webflash
+
+
+
+ + +
+ + +
+

목차

+
    +
  1. 프로젝트 개요
  2. +
  3. 시스템 아키텍처
  4. +
  5. 파일 구조 및 구성요소 분석
  6. +
  7. 기술 스택 상세
  8. +
  9. 핵심 기능 분석
  10. +
  11. API 명세
  12. +
  13. 데이터 흐름
  14. +
  15. 보안 분석
  16. +
  17. 배포 및 운영
  18. +
  19. 결론 및 확장 방향
  20. +
+
+ + +
+
+
1
+

프로젝트 개요

+
+ +

+ ESP32 Web Flasher는 ESP32 계열 마이크로컨트롤러에 펌웨어를 플래시하는 작업을 + 웹 브라우저에서 수행할 수 있게 해주는 Docker 기반 웹 애플리케이션입니다. + Arduino IDE, esptool.py 등 별도 소프트웨어 설치 없이 Chrome/Edge 브라우저와 + USB 케이블만으로 동작합니다. +

+ +
+
+
목적
+
펌웨어 배포·업데이트 과정을 웹 기반으로 단순화하여 설치 장벽 제거
+
+
+
핵심 기술
+
Web Serial API + Espressif esp-web-tools + Docker 멀티컨테이너
+
+
+
접근 URL
+
http://localhost:3100
브라우저 Chrome/Edge 89+ 필요
+
+
+ +

지원 칩 패밀리

+ + + + + + + + + + +
USB 방식비고
ESP32-S3내장 USB-OTG (드라이버 불필요)권장
ESP32-S2내장 USB-OTG지원
ESP32-C3USB-CDC지원
ESP32USB-UART 변환기 (CP2102 등)변환칩 필요
+
+ + +
+
+
2
+

시스템 아키텍처

+
+ +

+ 두 개의 Docker 컨테이너가 Docker Compose로 오케스트레이션됩니다. + Nginx가 리버스 프록시 역할을 하여 정적 파일 서빙과 API 라우팅을 모두 담당합니다. +

+ +
+
컨테이너 구성
+ + + + + + + + + + + + + + + + + + + + +
컨테이너이미지내부 포트외부 포트역할
esp32-flasher-frontendnginx:alpine803100정적 파일 서빙 + 리버스 프록시
esp32-flasher-backendnode:20-alpine3000비공개 (내부망)REST API + 파일 관리
+
+ +

통신 흐름

+
+
사용자 브라우저
Chrome/Edge
+
+
Frontend
Nginx :3100
+
+
Backend
Express :3000
+
+
스토리지
Docker Volume
+
+
+
사용자 브라우저
Web Serial API
+
+
USB 연결
CDC/OTG
+
+
ESP32
ROM Bootloader
+
+ +

Nginx 라우팅 규칙

+
location /api/      →  proxy_pass http://backend:3000/api/
+location /firmware/ →  proxy_pass http://backend:3000/firmware/
+location /          →  try_files $uri /index.html  (SPA 라우팅)
+ +

백엔드는 외부에 포트를 노출하지 않으며, Docker 내부 DNS(backend:3000)를 통해서만 접근됩니다.

+
+ + +
+
+
3
+

파일 구조 및 구성요소 분석

+
+ +
+webflash/ +├── backend/ +│ ├── Dockerfile # Node.js 20 Alpine 이미지 +│ ├── package.json # Express, Multer, UUID, CORS +│ └── server.js # REST API 서버 (188줄) +├── frontend/ +│ ├── Dockerfile # Nginx Alpine 이미지 +│ ├── nginx.conf # 리버스 프록시 설정 +│ ├── index.html # SPA (356줄, 한국어 UI) +│ ├── css/ +│ │ └── style.css # 다크 테마 (388줄) +│ └── js/ +│ └── app.js # Web Serial 로직 (396줄) +├── docker-compose.yml # 멀티컨테이너 오케스트레이션 +└── README.md # 프로젝트 문서 +
+ +
+
+
backend/server.js
+

+ Express.js 기반 REST API 서버. Multer로 멀티파트 업로드 처리, + UUID로 펌웨어 식별자 생성, 파일시스템 기반 JSON 메타데이터 관리. + esp-web-tools가 요구하는 manifest.json을 동적으로 생성하여 플래싱 파라미터를 제공합니다. +

+
+
+
frontend/js/app.js
+

+ Vanilla JavaScript로 작성된 SPA 로직. Web Serial API로 ESP32 연결을 확인하고, + XHR 기반 업로드 진행률 추적, 드래그&드롭 파일 선택, + esp-web-install-button 웹 컴포넌트 manifest 동적 주입을 담당합니다. +

+
+
+
frontend/nginx.conf
+

+ SPA 라우팅(try_files), API 역방향 프록시, 펌웨어 파일 서빙을 + 하나의 Nginx 설정으로 처리합니다. 백엔드는 Docker DNS를 통해 참조됩니다. +

+
+
+
docker-compose.yml
+

+ backend/frontend 두 서비스를 정의합니다. backend 헬스체크(30초 간격), + frontend는 backend depends_on, 펌웨어는 Named Volume으로 영속 저장됩니다. +

+
+
+
+ + +
+
+
4
+

기술 스택 상세

+
+ +
+
+
백엔드
+
+
🟩
+
+
Node.js 20 (Alpine)
+
LTS 버전, 경량 Alpine 이미지로 컨테이너 크기 최소화
+
+
+
+
+
+
Express 4.18.2
+
REST API 라우팅, 미들웨어 파이프라인
+
+
+
+
📤
+
+
Multer 1.4.5
+
multipart/form-data 파일 업로드, 8MB 제한, .bin 필터
+
+
+
+
🔑
+
+
UUID v9
+
펌웨어 고유 식별자 생성 (RFC 4122 v4)
+
+
+
+
🌐
+
+
CORS 2.8.5
+
크로스 오리진 요청 허용, ALLOWED_ORIGIN 환경변수로 제어
+
+
+
+ +
+
프론트엔드
+
+
🌍
+
+
Vanilla JavaScript (ES2020+)
+
프레임워크 없음, 번들러 없음 — 경량 직접 실행
+
+
+
+
🔌
+
+
Web Serial API
+
브라우저 네이티브 시리얼 통신, Chrome/Edge 89+ 지원
+
+
+
+
⚙️
+
+
esp-web-tools v10
+
Espressif 공식 웹 컴포넌트, manifest.json 기반 플래싱
+
+
+
+
🎨
+
+
CSS Custom Properties
+
다크 테마, GitHub Dark 팔레트, 반응형 그리드
+
+
+
+
🔀
+
+
Nginx Alpine
+
정적 서빙 + 리버스 프록시 (이미지 ~40 MB)
+
+
+
+
+ +

인프라

+ + + + + + + + + + +
구성요소기술버전용도
컨테이너 런타임Docker Engine20.10+컨테이너 빌드·실행
오케스트레이션Docker Composev2멀티서비스 관리
스토리지Docker Named Volume펌웨어 바이너리 영속 저장
메타데이터 DBJSON 파일 (_metadata.json)펌웨어 정보 레지스트리
+
+ + +
+
+
5
+

핵심 기능 분석

+
+ +

① 펌웨어 업로드 관리

+

+ multipart/form-data 방식으로 최대 3개의 .bin 파일(firmware, bootloader, partitions)을 동시 업로드합니다. + 각 파일은 타임스탬프-랜덤4바이트.bin 형태로 저장되고, + UUID 기반 메타데이터가 JSON 파일에 기록됩니다. +

+
{
+  "id": "550e8400-e29b-41d4-a716-446655440000",
+  "name": "My Product v1.0",
+  "version": "1.0.0",
+  "chipFamily": "ESP32-S3",
+  "createdAt": "2025-05-17T10:00:00.000Z",
+  "parts": [
+    { "file": "1747123456-ab12.bin", "offset": "0x0000",  "label": "Bootloader" },
+    { "file": "1747123456-cd34.bin", "offset": "0x8000",  "label": "Partition Table" },
+    { "file": "1747123456-ef56.bin", "offset": "0x10000", "label": "Application" }
+  ]
+}
+ +

② Manifest 동적 생성

+

+ 백엔드가 저장된 메타데이터를 esp-web-tools 표준 manifest 형식으로 변환합니다. + offset은 16진수 문자열에서 정수로 자동 변환됩니다. +

+
GET /api/firmware/:id/manifest
+
+{
+  "name": "My Product v1.0",
+  "version": "1.0.0",
+  "new_install_prompt_erase": true,
+  "builds": [{
+    "chipFamily": "ESP32-S3",
+    "parts": [
+      { "path": "/firmware/files/xxxx.bin", "offset": 0 },
+      { "path": "/firmware/files/yyyy.bin", "offset": 32768 },
+      { "path": "/firmware/files/zzzz.bin", "offset": 65536 }
+    ]
+  }]
+}
+ +

③ Web Serial API 연결 확인

+

+ 플래시 전에 ESP32가 올바르게 연결되었는지 VID/PID로 확인합니다. + Espressif의 USB Vendor ID는 0x303A로, 이 값을 감지하면 ESP32 장치로 판단합니다. +

+
+
장치 확인 로직
+ + + + + + + + +
VID장치비고
0x303AEspressif ESP32 (내장 USB)Espressif 확인
0x10C4Silicon Labs CP210xUSB-UART 변환기
0x1A86CH340/CH341USB-UART 변환기
기타알 수 없는 장치경고 표시
+
+ +

④ 드래그&드롭 파일 업로드

+

+ DataTransfer API를 이용한 드래그&드롭 인터페이스와 XHR 기반 업로드 진행률 추적을 구현합니다. + fetch()는 업로드 진행률을 지원하지 않으므로 XMLHttpRequest를 직접 사용합니다. +

+ +

⑤ 시리얼 모니터

+
+
로그 출력 유형
+ + + + + + + + +
타입CSS 클래스색상용도
okline-okteal (#00c896)성공 메시지
errline-errred (#f85149)오류 메시지
warnline-warnyellow (#e3b341)경고 메시지
infoline-infomuted (#8b949e)정보 메시지
+
+
+ + +
+
+
6
+

API 명세

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
메서드경로설명응답
GET/api/health헬스 체크 (Docker 프로브용){ status: "ok", timestamp }
GET/api/firmware펌웨어 목록 조회메타데이터 배열 JSON
POST/api/firmware/upload펌웨어 업로드 (multipart){ success, id, firmware }
GET/api/firmware/:id/manifestesp-web-tools manifest 생성manifest JSON
DELETE/api/firmware/:id펌웨어 삭제 (파일 + 메타데이터){ success: true }
GET/firmware/files/:filename바이너리 파일 정적 서빙application/octet-stream
+ +

업로드 필드 상세

+ + + + + + + + + + + + + +
필드명타입필수기본값설명
firmwarefile (.bin)필수애플리케이션 바이너리
bootloaderfile (.bin)선택부트로더 (offset 0x0000)
partitionsfile (.bin)선택파티션 테이블 (offset 0x8000)
namestring선택파일명펌웨어 이름
versionstring선택1.0.0버전 문자열
chipFamilystring선택ESP32-S3칩 패밀리
flashAddressstring (hex)선택0x10000앱 플래시 주소
+
+ + +
+
+
7
+

데이터 흐름

+
+ +

펌웨어 업로드 흐름

+
+
브라우저 FormData
+
+
Nginx 프록시
+
+
Multer 파싱
+
+
Disk 저장
+
+
JSON 메타 기록
+
+ +

플래싱 흐름

+
+
펌웨어 선택
+
+
manifest URL 주입
+
+
esp-web-tools 기동
+
+
manifest fetch
+
+
+
.bin 파일 fetch
+
+
Web Serial 연결
+
+
ESP32 ROM 부트로더
+
+
플래시 완료
+
+ +

+ 플래싱 단계에서 .bin 파일은 서버에서 브라우저로 다운로드된 후, + 브라우저가 직접 Web Serial API를 통해 ESP32에 씁니다. + 서버는 플래시 과정에 관여하지 않으므로 네트워크 지연의 영향이 최소화됩니다. +

+
+ + +
+
+
8
+

보안 분석

+
+ +

현재 구현된 보안 조치

+ + + + + + + + + + + + + + +
항목구현 여부방법
파일 유형 제한✓ 구현Multer fileFilter — .bin 확장자만 허용
파일 크기 제한✓ 구현8 MB 상한 (limits.fileSize)
XSS 방지✓ 구현escHtml() 함수로 사용자 입력 이스케이프
CORS 제어✓ 구현ALLOWED_ORIGIN 환경변수로 설정 가능
안전한 파일명✓ 구현원본 파일명 미사용, 타임스탬프+랜덤 생성
인증/인가미구현현재 MVP 단계, 토큰 인증 추가 필요
Rate Limiting미구현무차별 업로드 공격 방지 필요
HTTPS미구현외부 배포 시 필수 (현재 HTTP)
+ +

ESP32 하드웨어 보안 옵션

+
+
1
+
+
Flash Encryption
+
AES-256으로 플래시 메모리 암호화 — 덤프 시 암호화된 데이터만 읽힘
+
+
+
+
2
+
+
Secure Boot V2
+
부팅 시 서명 검증 — 미서명 펌웨어 실행 차단
+
+
+
+
3
+
+
NVS Encryption
+
비휘발성 저장소(설정 데이터) 암호화
+
+
+
+
4
+
+
JTAG 비활성화
+
eFuse로 디버그 인터페이스 영구 차단
+
+
+ +

추천 보안 강화 방향

+
# HTTPS (Nginx SSL 설정 예시)
+server {
+    listen 443 ssl;
+    ssl_certificate     /etc/ssl/cert.pem;
+    ssl_certificate_key /etc/ssl/key.pem;
+    ...
+}
+
+# Flash Encryption 활성화 (esptool)
+espefuse.py --chip esp32s3 burn_efuse DIS_DOWNLOAD_PLAIN_DECRYPT
+
+ + +
+
+
9
+

배포 및 운영

+
+ +

Docker 구성 상세

+ + + + + + + + + + + + +
항목백엔드프론트엔드
베이스 이미지node:20-alpinenginx:alpine
예상 이미지 크기~400 MB~40 MB
노출 포트없음 (내부 전용)3100:80
재시작 정책unless-stoppedunless-stopped
헬스 체크30초 간격 (3회 재시도)없음
볼륨firmware_uploads:/app/uploads없음
+ +

환경 변수

+ + + + + + + + + +
변수명기본값설명
PORT3000백엔드 리스닝 포트
NODE_ENVproductionNode.js 실행 환경
ALLOWED_ORIGIN* (전체 허용)CORS 허용 오리진
+ +

운영 명령

+
# 빌드 및 시작
+docker compose up -d --build
+
+# 로그 확인
+docker compose logs -f
+
+# 재시작
+docker compose restart
+
+# 중지 (데이터 유지)
+docker compose down
+
+# 완전 초기화 (데이터 삭제)
+docker compose down -v
+
+ + +
+
+
10
+

결론 및 확장 방향

+
+ +

현재 구현 평가

+
+
+
+
+
기술적 완성도
+
Web Serial API + esp-web-tools 연동, 멀티파트 업로드, 동적 manifest 생성이 모두 구현되어 있음
+
+
+
+
+
+
Docker 아키텍처
+
백엔드/프론트엔드 분리, 헬스 체크, Named Volume, 내부 네트워크 격리 적용
+
+
+
+
+
+
사용자 경험
+
한국어 UI, 다크 테마, 드래그&드롭, 실시간 진행률, 시리얼 모니터 제공
+
+
+
+
⚠️
+
+
브라우저 제한
+
Web Serial API는 Chrome/Edge 89+만 지원 — Firefox, Safari 미지원
+
+
+
+
⚠️
+
+
인증 미구현
+
현재 MVP 수준으로 인증 없이 누구나 업로드/삭제 가능
+
+
+
+ +

확장 로드맵

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
단계기능우선순위
v1.1HTTPS 지원 (Nginx SSL/TLS), 업로드 인증 토큰높음
v1.2Rate Limiting, 플래시 이력 로깅, MAC 주소 기반 기기 인증중간
v2.0결제 연동 (Stripe), 1회용 플래시 토큰, 라이선스 서버계획
v2.1S3 호환 스토리지 연동, CDN 배포, 다국어 UI계획
v3.0OTA 업데이트 통합, 구독 모델, 관리자 대시보드계획
+ +

상업화 가능성

+

+ Espressif 공식 esp-web-tools를 기반으로 하므로 기술 안정성이 높습니다. + ESP32-S3의 내장 USB-OTG 지원으로 별도 USB-UART 변환 칩 없이 플래싱이 가능하고, + 이는 제품 원가 절감으로 이어집니다. + MAC 주소 기반 라이선스 바인딩(ESP32의 eFuse MAC은 위변조 난이도 높음)과 + 결합하면 펌웨어 유료 판매 플랫폼으로 확장할 수 있습니다. +

+

+ 현재 MVP 아키텍처(파일 기반 JSON 메타데이터)는 소규모 운영에 적합하며, + 수백~수천 건 이상의 트래픽이 예상되면 PostgreSQL 등 관계형 DB와 S3 호환 스토리지로 + 마이그레이션이 권장됩니다. +

+
+ +
+

+ ESP32 Web Flasher 기술 분석 보고서 — 2025년 5월 작성 +

+ +
+ +