- README.md: project overview, quick start, usage guide, API reference, directory structure, environment variables, security notes - report.html: comprehensive Korean technical report covering architecture, tech stack, feature analysis, API spec, data flow, security, deployment, and roadmap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1068 lines
41 KiB
HTML
1068 lines
41 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>ESP32 Web Flasher — 기술 분석 보고서</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--surface2: #21262d;
|
|
--border: #30363d;
|
|
--accent: #00c896;
|
|
--accent2: #58a6ff;
|
|
--danger: #f85149;
|
|
--warn: #e3b341;
|
|
--text: #e6edf3;
|
|
--muted: #8b949e;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
|
|
font-size: 15px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
/* ── 커버 페이지 ── */
|
|
.cover {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
text-align: center;
|
|
background: radial-gradient(ellipse at 50% 0%, #0e2a23 0%, var(--bg) 70%);
|
|
padding: 60px 24px;
|
|
}
|
|
.cover-logo {
|
|
width: 96px; height: 96px;
|
|
margin-bottom: 32px;
|
|
}
|
|
.cover h1 {
|
|
font-size: 2.6rem;
|
|
font-weight: 700;
|
|
letter-spacing: -0.5px;
|
|
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
margin-bottom: 12px;
|
|
}
|
|
.cover .subtitle {
|
|
font-size: 1.1rem;
|
|
color: var(--muted);
|
|
max-width: 520px;
|
|
margin-bottom: 40px;
|
|
}
|
|
.cover .meta-grid {
|
|
display: flex;
|
|
gap: 40px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
}
|
|
.cover .meta-item { text-align: center; }
|
|
.cover .meta-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; }
|
|
.cover .meta-value { font-size: 14px; color: var(--accent); font-weight: 600; margin-top: 4px; }
|
|
|
|
/* ── 레이아웃 ── */
|
|
.container {
|
|
max-width: 1020px;
|
|
margin: 0 auto;
|
|
padding: 48px 24px 80px;
|
|
}
|
|
|
|
/* ── 섹션 헤더 ── */
|
|
.section {
|
|
margin-bottom: 56px;
|
|
}
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin-bottom: 28px;
|
|
padding-bottom: 14px;
|
|
border-bottom: 2px solid var(--border);
|
|
}
|
|
.section-num {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 36px; height: 36px;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, var(--accent), #00a87a);
|
|
color: #000;
|
|
font-weight: 800;
|
|
font-size: 15px;
|
|
flex-shrink: 0;
|
|
}
|
|
.section-header h2 {
|
|
font-size: 1.35rem;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
margin: 24px 0 10px;
|
|
}
|
|
p { margin-bottom: 14px; color: var(--muted); }
|
|
strong { color: var(--text); }
|
|
|
|
/* ── 카드 ── */
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 20px 24px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.card-title {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
color: var(--muted);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* ── 그리드 ── */
|
|
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
|
|
@media(max-width: 720px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
|
|
|
|
/* ── 테이블 ── */
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13.5px;
|
|
margin-bottom: 16px;
|
|
}
|
|
th {
|
|
background: var(--surface2);
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
padding: 10px 14px;
|
|
text-align: left;
|
|
border: 1px solid var(--border);
|
|
}
|
|
td {
|
|
padding: 10px 14px;
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
vertical-align: top;
|
|
}
|
|
tr:nth-child(even) td { background: rgba(255,255,255,.02); }
|
|
td code {
|
|
background: var(--surface2);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
|
}
|
|
|
|
/* ── 코드 블록 ── */
|
|
pre {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px 18px;
|
|
overflow-x: auto;
|
|
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
|
font-size: 12.5px;
|
|
line-height: 1.6;
|
|
color: #c9d1d9;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* ── 배지 ── */
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 3px 9px;
|
|
border-radius: 20px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.badge-ok { background: rgba(0,200,150,.15); color: var(--accent); }
|
|
.badge-warn { background: rgba(227,179,65,.15); color: var(--warn); }
|
|
.badge-danger { background: rgba(248,81,73,.15); color: var(--danger); }
|
|
.badge-info { background: rgba(88,166,255,.15); color: var(--accent2); }
|
|
|
|
/* ── 플로우 다이어그램 ── */
|
|
.flow {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0;
|
|
margin: 16px 0;
|
|
}
|
|
.flow-box {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px 16px;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
min-width: 110px;
|
|
}
|
|
.flow-box .label { color: var(--muted); font-size: 10px; text-transform: uppercase; }
|
|
.flow-box .value { color: var(--text); font-weight: 600; }
|
|
.flow-arrow {
|
|
color: var(--border);
|
|
font-size: 20px;
|
|
padding: 0 6px;
|
|
}
|
|
|
|
/* ── 스택 아이템 ── */
|
|
.stack-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 14px;
|
|
padding: 14px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.stack-item:last-child { border-bottom: none; }
|
|
.stack-icon {
|
|
width: 38px; height: 38px;
|
|
border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 18px;
|
|
background: var(--surface2);
|
|
flex-shrink: 0;
|
|
}
|
|
.stack-info .name { font-weight: 600; font-size: 14px; }
|
|
.stack-info .desc { font-size: 12px; color: var(--muted); margin-top: 3px; }
|
|
|
|
/* ── 파일 트리 ── */
|
|
.filetree {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 18px 20px;
|
|
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
|
font-size: 13px;
|
|
line-height: 2;
|
|
}
|
|
.ft-dir { color: var(--accent2); }
|
|
.ft-file { color: var(--text); }
|
|
.ft-comment { color: var(--muted); }
|
|
|
|
/* ── 보안 레이어 ── */
|
|
.security-layer {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 12px 16px;
|
|
background: var(--surface2);
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
border-left: 3px solid var(--accent);
|
|
}
|
|
.layer-num {
|
|
width: 28px; height: 28px;
|
|
border-radius: 50%;
|
|
background: rgba(0,200,150,.15);
|
|
color: var(--accent);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 12px; font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
.layer-info .name { font-size: 13px; font-weight: 600; }
|
|
.layer-info .desc { font-size: 12px; color: var(--muted); }
|
|
|
|
/* ── 결론 카드 ── */
|
|
.conclusion-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.conclusion-item:last-child { border-bottom: none; }
|
|
.conclusion-icon { font-size: 20px; }
|
|
.conclusion-text .label { font-size: 13px; font-weight: 600; }
|
|
.conclusion-text .value { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
|
|
|
/* ── TOC ── */
|
|
.toc {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 24px 28px;
|
|
margin-bottom: 48px;
|
|
}
|
|
.toc h2 { font-size: 14px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 14px; }
|
|
.toc ol { padding-left: 20px; }
|
|
.toc li { margin-bottom: 7px; }
|
|
.toc a { color: var(--accent2); text-decoration: none; font-size: 14px; }
|
|
.toc a:hover { text-decoration: underline; }
|
|
|
|
/* ── 구분선 ── */
|
|
hr { border: none; border-top: 1px solid var(--border); margin: 28px 0; }
|
|
|
|
/* ── 인쇄 ── */
|
|
@media print {
|
|
body { background: #fff; color: #111; }
|
|
.cover { background: #f5f5f5; }
|
|
.cover h1 { -webkit-text-fill-color: #007a5e; }
|
|
.card, .filetree, pre { border-color: #ddd; background: #f9f9f9; }
|
|
th { background: #eee; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ══════════════════════ 커버 ══════════════════════ -->
|
|
<div class="cover">
|
|
<svg class="cover-logo" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="96" height="96" rx="18" fill="#00c896" fill-opacity=".12"/>
|
|
<rect x="18" y="18" width="60" height="60" rx="9" stroke="#00c896" stroke-width="3"/>
|
|
<rect x="30" y="30" width="15" height="15" fill="#00c896"/>
|
|
<rect x="51" y="30" width="15" height="15" fill="#00c896" fill-opacity=".5"/>
|
|
<rect x="30" y="51" width="15" height="15" fill="#00c896" fill-opacity=".5"/>
|
|
<rect x="51" y="51" width="15" height="15" fill="#00c896"/>
|
|
<line x1="6" y1="36" x2="18" y2="36" stroke="#00c896" stroke-width="2.5"/>
|
|
<line x1="6" y1="60" x2="18" y2="60" stroke="#00c896" stroke-width="2.5"/>
|
|
<line x1="78" y1="36" x2="90" y2="36" stroke="#00c896" stroke-width="2.5"/>
|
|
<line x1="78" y1="60" x2="90" y2="60" stroke="#00c896" stroke-width="2.5"/>
|
|
</svg>
|
|
<h1>ESP32 Web Flasher</h1>
|
|
<p class="subtitle">
|
|
브라우저 기반 ESP32 펌웨어 플래시 시스템<br>
|
|
Docker 컨테이너 아키텍처 기술 분석 보고서
|
|
</p>
|
|
<div class="meta-grid">
|
|
<div class="meta-item">
|
|
<div class="meta-label">작성일</div>
|
|
<div class="meta-value">2025년 5월</div>
|
|
</div>
|
|
<div class="meta-item">
|
|
<div class="meta-label">버전</div>
|
|
<div class="meta-value">v1.0.0</div>
|
|
</div>
|
|
<div class="meta-item">
|
|
<div class="meta-label">분류</div>
|
|
<div class="meta-value">기술 분석</div>
|
|
</div>
|
|
<div class="meta-item">
|
|
<div class="meta-label">저장소</div>
|
|
<div class="meta-value">gitea.byunc.com/byun/webflash</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ══════════════════════ 본문 ══════════════════════ -->
|
|
<div class="container">
|
|
|
|
<!-- 목차 -->
|
|
<div class="toc">
|
|
<h2>목차</h2>
|
|
<ol>
|
|
<li><a href="#s1">프로젝트 개요</a></li>
|
|
<li><a href="#s2">시스템 아키텍처</a></li>
|
|
<li><a href="#s3">파일 구조 및 구성요소 분석</a></li>
|
|
<li><a href="#s4">기술 스택 상세</a></li>
|
|
<li><a href="#s5">핵심 기능 분석</a></li>
|
|
<li><a href="#s6">API 명세</a></li>
|
|
<li><a href="#s7">데이터 흐름</a></li>
|
|
<li><a href="#s8">보안 분석</a></li>
|
|
<li><a href="#s9">배포 및 운영</a></li>
|
|
<li><a href="#s10">결론 및 확장 방향</a></li>
|
|
</ol>
|
|
</div>
|
|
|
|
<!-- 1. 프로젝트 개요 -->
|
|
<div class="section" id="s1">
|
|
<div class="section-header">
|
|
<div class="section-num">1</div>
|
|
<h2>프로젝트 개요</h2>
|
|
</div>
|
|
|
|
<p>
|
|
<strong>ESP32 Web Flasher</strong>는 ESP32 계열 마이크로컨트롤러에 펌웨어를 플래시하는 작업을
|
|
웹 브라우저에서 수행할 수 있게 해주는 Docker 기반 웹 애플리케이션입니다.
|
|
Arduino IDE, esptool.py 등 별도 소프트웨어 설치 없이 Chrome/Edge 브라우저와
|
|
USB 케이블만으로 동작합니다.
|
|
</p>
|
|
|
|
<div class="grid3">
|
|
<div class="card">
|
|
<div class="card-title">목적</div>
|
|
<div>펌웨어 배포·업데이트 과정을 웹 기반으로 단순화하여 설치 장벽 제거</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">핵심 기술</div>
|
|
<div>Web Serial API + Espressif esp-web-tools + Docker 멀티컨테이너</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">접근 URL</div>
|
|
<div><code>http://localhost:3100</code><br>브라우저 Chrome/Edge 89+ 필요</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>지원 칩 패밀리</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>칩</th><th>USB 방식</th><th>비고</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><strong>ESP32-S3</strong></td><td>내장 USB-OTG (드라이버 불필요)</td><td><span class="badge badge-ok">권장</span></td></tr>
|
|
<tr><td>ESP32-S2</td><td>내장 USB-OTG</td><td><span class="badge badge-ok">지원</span></td></tr>
|
|
<tr><td>ESP32-C3</td><td>USB-CDC</td><td><span class="badge badge-ok">지원</span></td></tr>
|
|
<tr><td>ESP32</td><td>USB-UART 변환기 (CP2102 등)</td><td><span class="badge badge-warn">변환칩 필요</span></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 2. 시스템 아키텍처 -->
|
|
<div class="section" id="s2">
|
|
<div class="section-header">
|
|
<div class="section-num">2</div>
|
|
<h2>시스템 아키텍처</h2>
|
|
</div>
|
|
|
|
<p>
|
|
두 개의 Docker 컨테이너가 Docker Compose로 오케스트레이션됩니다.
|
|
Nginx가 리버스 프록시 역할을 하여 정적 파일 서빙과 API 라우팅을 모두 담당합니다.
|
|
</p>
|
|
|
|
<div class="card">
|
|
<div class="card-title">컨테이너 구성</div>
|
|
<table>
|
|
<thead>
|
|
<tr><th>컨테이너</th><th>이미지</th><th>내부 포트</th><th>외부 포트</th><th>역할</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>esp32-flasher-frontend</code></td>
|
|
<td>nginx:alpine</td>
|
|
<td>80</td>
|
|
<td><strong>3100</strong></td>
|
|
<td>정적 파일 서빙 + 리버스 프록시</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>esp32-flasher-backend</code></td>
|
|
<td>node:20-alpine</td>
|
|
<td>3000</td>
|
|
<td>비공개 (내부망)</td>
|
|
<td>REST API + 파일 관리</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3>통신 흐름</h3>
|
|
<div class="flow">
|
|
<div class="flow-box"><div class="label">사용자 브라우저</div><div class="value">Chrome/Edge</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">Frontend</div><div class="value">Nginx :3100</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">Backend</div><div class="value">Express :3000</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">스토리지</div><div class="value">Docker Volume</div></div>
|
|
</div>
|
|
<div class="flow" style="margin-top: 8px;">
|
|
<div class="flow-box"><div class="label">사용자 브라우저</div><div class="value">Web Serial API</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">USB 연결</div><div class="value">CDC/OTG</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">ESP32</div><div class="value">ROM Bootloader</div></div>
|
|
</div>
|
|
|
|
<h3>Nginx 라우팅 규칙</h3>
|
|
<pre>location /api/ → proxy_pass http://backend:3000/api/
|
|
location /firmware/ → proxy_pass http://backend:3000/firmware/
|
|
location / → try_files $uri /index.html (SPA 라우팅)</pre>
|
|
|
|
<p>백엔드는 외부에 포트를 노출하지 않으며, Docker 내부 DNS(<code>backend:3000</code>)를 통해서만 접근됩니다.</p>
|
|
</div>
|
|
|
|
<!-- 3. 파일 구조 -->
|
|
<div class="section" id="s3">
|
|
<div class="section-header">
|
|
<div class="section-num">3</div>
|
|
<h2>파일 구조 및 구성요소 분석</h2>
|
|
</div>
|
|
|
|
<div class="filetree">
|
|
<span class="ft-dir">webflash/</span>
|
|
├── <span class="ft-dir">backend/</span>
|
|
│ ├── <span class="ft-file">Dockerfile</span> <span class="ft-comment"># Node.js 20 Alpine 이미지</span>
|
|
│ ├── <span class="ft-file">package.json</span> <span class="ft-comment"># Express, Multer, UUID, CORS</span>
|
|
│ └── <span class="ft-file">server.js</span> <span class="ft-comment"># REST API 서버 (188줄)</span>
|
|
├── <span class="ft-dir">frontend/</span>
|
|
│ ├── <span class="ft-file">Dockerfile</span> <span class="ft-comment"># Nginx Alpine 이미지</span>
|
|
│ ├── <span class="ft-file">nginx.conf</span> <span class="ft-comment"># 리버스 프록시 설정</span>
|
|
│ ├── <span class="ft-file">index.html</span> <span class="ft-comment"># SPA (356줄, 한국어 UI)</span>
|
|
│ ├── <span class="ft-dir">css/</span>
|
|
│ │ └── <span class="ft-file">style.css</span> <span class="ft-comment"># 다크 테마 (388줄)</span>
|
|
│ └── <span class="ft-dir">js/</span>
|
|
│ └── <span class="ft-file">app.js</span> <span class="ft-comment"># Web Serial 로직 (396줄)</span>
|
|
├── <span class="ft-file">docker-compose.yml</span> <span class="ft-comment"># 멀티컨테이너 오케스트레이션</span>
|
|
└── <span class="ft-file">README.md</span> <span class="ft-comment"># 프로젝트 문서</span>
|
|
</div>
|
|
|
|
<div class="grid2" style="margin-top: 16px;">
|
|
<div class="card">
|
|
<div class="card-title">backend/server.js</div>
|
|
<p style="margin:0;font-size:13px;">
|
|
Express.js 기반 REST API 서버. Multer로 멀티파트 업로드 처리,
|
|
UUID로 펌웨어 식별자 생성, 파일시스템 기반 JSON 메타데이터 관리.
|
|
esp-web-tools가 요구하는 manifest.json을 동적으로 생성하여 플래싱 파라미터를 제공합니다.
|
|
</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">frontend/js/app.js</div>
|
|
<p style="margin:0;font-size:13px;">
|
|
Vanilla JavaScript로 작성된 SPA 로직. Web Serial API로 ESP32 연결을 확인하고,
|
|
XHR 기반 업로드 진행률 추적, 드래그&드롭 파일 선택,
|
|
esp-web-install-button 웹 컴포넌트 manifest 동적 주입을 담당합니다.
|
|
</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">frontend/nginx.conf</div>
|
|
<p style="margin:0;font-size:13px;">
|
|
SPA 라우팅(try_files), API 역방향 프록시, 펌웨어 파일 서빙을
|
|
하나의 Nginx 설정으로 처리합니다. 백엔드는 Docker DNS를 통해 참조됩니다.
|
|
</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title">docker-compose.yml</div>
|
|
<p style="margin:0;font-size:13px;">
|
|
backend/frontend 두 서비스를 정의합니다. backend 헬스체크(30초 간격),
|
|
frontend는 backend depends_on, 펌웨어는 Named Volume으로 영속 저장됩니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 4. 기술 스택 -->
|
|
<div class="section" id="s4">
|
|
<div class="section-header">
|
|
<div class="section-num">4</div>
|
|
<h2>기술 스택 상세</h2>
|
|
</div>
|
|
|
|
<div class="grid2">
|
|
<div class="card">
|
|
<div class="card-title">백엔드</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">🟩</div>
|
|
<div class="stack-info">
|
|
<div class="name">Node.js 20 (Alpine)</div>
|
|
<div class="desc">LTS 버전, 경량 Alpine 이미지로 컨테이너 크기 최소화</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">⚡</div>
|
|
<div class="stack-info">
|
|
<div class="name">Express 4.18.2</div>
|
|
<div class="desc">REST API 라우팅, 미들웨어 파이프라인</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">📤</div>
|
|
<div class="stack-info">
|
|
<div class="name">Multer 1.4.5</div>
|
|
<div class="desc">multipart/form-data 파일 업로드, 8MB 제한, .bin 필터</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">🔑</div>
|
|
<div class="stack-info">
|
|
<div class="name">UUID v9</div>
|
|
<div class="desc">펌웨어 고유 식별자 생성 (RFC 4122 v4)</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">🌐</div>
|
|
<div class="stack-info">
|
|
<div class="name">CORS 2.8.5</div>
|
|
<div class="desc">크로스 오리진 요청 허용, ALLOWED_ORIGIN 환경변수로 제어</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">프론트엔드</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">🌍</div>
|
|
<div class="stack-info">
|
|
<div class="name">Vanilla JavaScript (ES2020+)</div>
|
|
<div class="desc">프레임워크 없음, 번들러 없음 — 경량 직접 실행</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">🔌</div>
|
|
<div class="stack-info">
|
|
<div class="name">Web Serial API</div>
|
|
<div class="desc">브라우저 네이티브 시리얼 통신, Chrome/Edge 89+ 지원</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">⚙️</div>
|
|
<div class="stack-info">
|
|
<div class="name">esp-web-tools v10</div>
|
|
<div class="desc">Espressif 공식 웹 컴포넌트, manifest.json 기반 플래싱</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">🎨</div>
|
|
<div class="stack-info">
|
|
<div class="name">CSS Custom Properties</div>
|
|
<div class="desc">다크 테마, GitHub Dark 팔레트, 반응형 그리드</div>
|
|
</div>
|
|
</div>
|
|
<div class="stack-item">
|
|
<div class="stack-icon">🔀</div>
|
|
<div class="stack-info">
|
|
<div class="name">Nginx Alpine</div>
|
|
<div class="desc">정적 서빙 + 리버스 프록시 (이미지 ~40 MB)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>인프라</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>구성요소</th><th>기술</th><th>버전</th><th>용도</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td>컨테이너 런타임</td><td>Docker Engine</td><td>20.10+</td><td>컨테이너 빌드·실행</td></tr>
|
|
<tr><td>오케스트레이션</td><td>Docker Compose</td><td>v2</td><td>멀티서비스 관리</td></tr>
|
|
<tr><td>스토리지</td><td>Docker Named Volume</td><td>—</td><td>펌웨어 바이너리 영속 저장</td></tr>
|
|
<tr><td>메타데이터 DB</td><td>JSON 파일 (_metadata.json)</td><td>—</td><td>펌웨어 정보 레지스트리</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 5. 핵심 기능 분석 -->
|
|
<div class="section" id="s5">
|
|
<div class="section-header">
|
|
<div class="section-num">5</div>
|
|
<h2>핵심 기능 분석</h2>
|
|
</div>
|
|
|
|
<h3>① 펌웨어 업로드 관리</h3>
|
|
<p>
|
|
multipart/form-data 방식으로 최대 3개의 .bin 파일(firmware, bootloader, partitions)을 동시 업로드합니다.
|
|
각 파일은 <code>타임스탬프-랜덤4바이트.bin</code> 형태로 저장되고,
|
|
UUID 기반 메타데이터가 JSON 파일에 기록됩니다.
|
|
</p>
|
|
<pre>{
|
|
"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" }
|
|
]
|
|
}</pre>
|
|
|
|
<h3>② Manifest 동적 생성</h3>
|
|
<p>
|
|
백엔드가 저장된 메타데이터를 esp-web-tools 표준 manifest 형식으로 변환합니다.
|
|
offset은 16진수 문자열에서 정수로 자동 변환됩니다.
|
|
</p>
|
|
<pre>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 }
|
|
]
|
|
}]
|
|
}</pre>
|
|
|
|
<h3>③ Web Serial API 연결 확인</h3>
|
|
<p>
|
|
플래시 전에 ESP32가 올바르게 연결되었는지 VID/PID로 확인합니다.
|
|
Espressif의 USB Vendor ID는 <code>0x303A</code>로, 이 값을 감지하면 ESP32 장치로 판단합니다.
|
|
</p>
|
|
<div class="card">
|
|
<div class="card-title">장치 확인 로직</div>
|
|
<table>
|
|
<thead><tr><th>VID</th><th>장치</th><th>비고</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><code>0x303A</code></td><td>Espressif ESP32 (내장 USB)</td><td><span class="badge badge-ok">Espressif 확인</span></td></tr>
|
|
<tr><td><code>0x10C4</code></td><td>Silicon Labs CP210x</td><td><span class="badge badge-warn">USB-UART 변환기</span></td></tr>
|
|
<tr><td><code>0x1A86</code></td><td>CH340/CH341</td><td><span class="badge badge-warn">USB-UART 변환기</span></td></tr>
|
|
<tr><td>기타</td><td>알 수 없는 장치</td><td><span class="badge badge-info">경고 표시</span></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3>④ 드래그&드롭 파일 업로드</h3>
|
|
<p>
|
|
DataTransfer API를 이용한 드래그&드롭 인터페이스와 XHR 기반 업로드 진행률 추적을 구현합니다.
|
|
<code>fetch()</code>는 업로드 진행률을 지원하지 않으므로 XMLHttpRequest를 직접 사용합니다.
|
|
</p>
|
|
|
|
<h3>⑤ 시리얼 모니터</h3>
|
|
<div class="card">
|
|
<div class="card-title">로그 출력 유형</div>
|
|
<table>
|
|
<thead><tr><th>타입</th><th>CSS 클래스</th><th>색상</th><th>용도</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>ok</td><td><code>line-ok</code></td><td>teal (#00c896)</td><td>성공 메시지</td></tr>
|
|
<tr><td>err</td><td><code>line-err</code></td><td>red (#f85149)</td><td>오류 메시지</td></tr>
|
|
<tr><td>warn</td><td><code>line-warn</code></td><td>yellow (#e3b341)</td><td>경고 메시지</td></tr>
|
|
<tr><td>info</td><td><code>line-info</code></td><td>muted (#8b949e)</td><td>정보 메시지</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 6. API 명세 -->
|
|
<div class="section" id="s6">
|
|
<div class="section-header">
|
|
<div class="section-num">6</div>
|
|
<h2>API 명세</h2>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>메서드</th><th>경로</th><th>설명</th><th>응답</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><span class="badge badge-ok">GET</span></td>
|
|
<td><code>/api/health</code></td>
|
|
<td>헬스 체크 (Docker 프로브용)</td>
|
|
<td><code>{ status: "ok", timestamp }</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge badge-ok">GET</span></td>
|
|
<td><code>/api/firmware</code></td>
|
|
<td>펌웨어 목록 조회</td>
|
|
<td>메타데이터 배열 JSON</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge badge-info">POST</span></td>
|
|
<td><code>/api/firmware/upload</code></td>
|
|
<td>펌웨어 업로드 (multipart)</td>
|
|
<td><code>{ success, id, firmware }</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge badge-ok">GET</span></td>
|
|
<td><code>/api/firmware/:id/manifest</code></td>
|
|
<td>esp-web-tools manifest 생성</td>
|
|
<td>manifest JSON</td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge badge-danger">DELETE</span></td>
|
|
<td><code>/api/firmware/:id</code></td>
|
|
<td>펌웨어 삭제 (파일 + 메타데이터)</td>
|
|
<td><code>{ success: true }</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><span class="badge badge-ok">GET</span></td>
|
|
<td><code>/firmware/files/:filename</code></td>
|
|
<td>바이너리 파일 정적 서빙</td>
|
|
<td>application/octet-stream</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3>업로드 필드 상세</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>필드명</th><th>타입</th><th>필수</th><th>기본값</th><th>설명</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>firmware</code></td><td>file (.bin)</td><td><span class="badge badge-danger">필수</span></td><td>—</td><td>애플리케이션 바이너리</td></tr>
|
|
<tr><td><code>bootloader</code></td><td>file (.bin)</td><td>선택</td><td>—</td><td>부트로더 (offset 0x0000)</td></tr>
|
|
<tr><td><code>partitions</code></td><td>file (.bin)</td><td>선택</td><td>—</td><td>파티션 테이블 (offset 0x8000)</td></tr>
|
|
<tr><td><code>name</code></td><td>string</td><td>선택</td><td>파일명</td><td>펌웨어 이름</td></tr>
|
|
<tr><td><code>version</code></td><td>string</td><td>선택</td><td>1.0.0</td><td>버전 문자열</td></tr>
|
|
<tr><td><code>chipFamily</code></td><td>string</td><td>선택</td><td>ESP32-S3</td><td>칩 패밀리</td></tr>
|
|
<tr><td><code>flashAddress</code></td><td>string (hex)</td><td>선택</td><td>0x10000</td><td>앱 플래시 주소</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 7. 데이터 흐름 -->
|
|
<div class="section" id="s7">
|
|
<div class="section-header">
|
|
<div class="section-num">7</div>
|
|
<h2>데이터 흐름</h2>
|
|
</div>
|
|
|
|
<h3>펌웨어 업로드 흐름</h3>
|
|
<div class="flow" style="flex-wrap:wrap;gap:4px;">
|
|
<div class="flow-box"><div class="label">①</div><div class="value">브라우저 FormData</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">②</div><div class="value">Nginx 프록시</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">③</div><div class="value">Multer 파싱</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">④</div><div class="value">Disk 저장</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">⑤</div><div class="value">JSON 메타 기록</div></div>
|
|
</div>
|
|
|
|
<h3>플래싱 흐름</h3>
|
|
<div class="flow" style="flex-wrap:wrap;gap:4px;margin-bottom:8px;">
|
|
<div class="flow-box"><div class="label">①</div><div class="value">펌웨어 선택</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">②</div><div class="value">manifest URL 주입</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">③</div><div class="value">esp-web-tools 기동</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">④</div><div class="value">manifest fetch</div></div>
|
|
</div>
|
|
<div class="flow" style="flex-wrap:wrap;gap:4px;">
|
|
<div class="flow-box"><div class="label">⑤</div><div class="value">.bin 파일 fetch</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">⑥</div><div class="value">Web Serial 연결</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">⑦</div><div class="value">ESP32 ROM 부트로더</div></div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-box"><div class="label">⑧</div><div class="value">플래시 완료</div></div>
|
|
</div>
|
|
|
|
<p style="margin-top: 16px;">
|
|
플래싱 단계에서 .bin 파일은 서버에서 브라우저로 다운로드된 후,
|
|
브라우저가 직접 Web Serial API를 통해 ESP32에 씁니다.
|
|
서버는 플래시 과정에 관여하지 않으므로 네트워크 지연의 영향이 최소화됩니다.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 8. 보안 분석 -->
|
|
<div class="section" id="s8">
|
|
<div class="section-header">
|
|
<div class="section-num">8</div>
|
|
<h2>보안 분석</h2>
|
|
</div>
|
|
|
|
<h3>현재 구현된 보안 조치</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>항목</th><th>구현 여부</th><th>방법</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td>파일 유형 제한</td><td><span class="badge badge-ok">✓ 구현</span></td><td>Multer fileFilter — .bin 확장자만 허용</td></tr>
|
|
<tr><td>파일 크기 제한</td><td><span class="badge badge-ok">✓ 구현</span></td><td>8 MB 상한 (limits.fileSize)</td></tr>
|
|
<tr><td>XSS 방지</td><td><span class="badge badge-ok">✓ 구현</span></td><td>escHtml() 함수로 사용자 입력 이스케이프</td></tr>
|
|
<tr><td>CORS 제어</td><td><span class="badge badge-ok">✓ 구현</span></td><td>ALLOWED_ORIGIN 환경변수로 설정 가능</td></tr>
|
|
<tr><td>안전한 파일명</td><td><span class="badge badge-ok">✓ 구현</span></td><td>원본 파일명 미사용, 타임스탬프+랜덤 생성</td></tr>
|
|
<tr><td>인증/인가</td><td><span class="badge badge-warn">미구현</span></td><td>현재 MVP 단계, 토큰 인증 추가 필요</td></tr>
|
|
<tr><td>Rate Limiting</td><td><span class="badge badge-warn">미구현</span></td><td>무차별 업로드 공격 방지 필요</td></tr>
|
|
<tr><td>HTTPS</td><td><span class="badge badge-warn">미구현</span></td><td>외부 배포 시 필수 (현재 HTTP)</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3>ESP32 하드웨어 보안 옵션</h3>
|
|
<div class="security-layer">
|
|
<div class="layer-num">1</div>
|
|
<div class="layer-info">
|
|
<div class="name">Flash Encryption</div>
|
|
<div class="desc">AES-256으로 플래시 메모리 암호화 — 덤프 시 암호화된 데이터만 읽힘</div>
|
|
</div>
|
|
</div>
|
|
<div class="security-layer">
|
|
<div class="layer-num">2</div>
|
|
<div class="layer-info">
|
|
<div class="name">Secure Boot V2</div>
|
|
<div class="desc">부팅 시 서명 검증 — 미서명 펌웨어 실행 차단</div>
|
|
</div>
|
|
</div>
|
|
<div class="security-layer">
|
|
<div class="layer-num">3</div>
|
|
<div class="layer-info">
|
|
<div class="name">NVS Encryption</div>
|
|
<div class="desc">비휘발성 저장소(설정 데이터) 암호화</div>
|
|
</div>
|
|
</div>
|
|
<div class="security-layer">
|
|
<div class="layer-num">4</div>
|
|
<div class="layer-info">
|
|
<div class="name">JTAG 비활성화</div>
|
|
<div class="desc">eFuse로 디버그 인터페이스 영구 차단</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>추천 보안 강화 방향</h3>
|
|
<pre># 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</pre>
|
|
</div>
|
|
|
|
<!-- 9. 배포 및 운영 -->
|
|
<div class="section" id="s9">
|
|
<div class="section-header">
|
|
<div class="section-num">9</div>
|
|
<h2>배포 및 운영</h2>
|
|
</div>
|
|
|
|
<h3>Docker 구성 상세</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>항목</th><th>백엔드</th><th>프론트엔드</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td>베이스 이미지</td><td>node:20-alpine</td><td>nginx:alpine</td></tr>
|
|
<tr><td>예상 이미지 크기</td><td>~400 MB</td><td>~40 MB</td></tr>
|
|
<tr><td>노출 포트</td><td>없음 (내부 전용)</td><td>3100:80</td></tr>
|
|
<tr><td>재시작 정책</td><td>unless-stopped</td><td>unless-stopped</td></tr>
|
|
<tr><td>헬스 체크</td><td>30초 간격 (3회 재시도)</td><td>없음</td></tr>
|
|
<tr><td>볼륨</td><td>firmware_uploads:/app/uploads</td><td>없음</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3>환경 변수</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>변수명</th><th>기본값</th><th>설명</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>PORT</code></td><td>3000</td><td>백엔드 리스닝 포트</td></tr>
|
|
<tr><td><code>NODE_ENV</code></td><td>production</td><td>Node.js 실행 환경</td></tr>
|
|
<tr><td><code>ALLOWED_ORIGIN</code></td><td>* (전체 허용)</td><td>CORS 허용 오리진</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3>운영 명령</h3>
|
|
<pre># 빌드 및 시작
|
|
docker compose up -d --build
|
|
|
|
# 로그 확인
|
|
docker compose logs -f
|
|
|
|
# 재시작
|
|
docker compose restart
|
|
|
|
# 중지 (데이터 유지)
|
|
docker compose down
|
|
|
|
# 완전 초기화 (데이터 삭제)
|
|
docker compose down -v</pre>
|
|
</div>
|
|
|
|
<!-- 10. 결론 -->
|
|
<div class="section" id="s10">
|
|
<div class="section-header">
|
|
<div class="section-num">10</div>
|
|
<h2>결론 및 확장 방향</h2>
|
|
</div>
|
|
|
|
<h3>현재 구현 평가</h3>
|
|
<div class="card">
|
|
<div class="conclusion-item">
|
|
<div class="conclusion-icon">✅</div>
|
|
<div class="conclusion-text">
|
|
<div class="label">기술적 완성도</div>
|
|
<div class="value">Web Serial API + esp-web-tools 연동, 멀티파트 업로드, 동적 manifest 생성이 모두 구현되어 있음</div>
|
|
</div>
|
|
</div>
|
|
<div class="conclusion-item">
|
|
<div class="conclusion-icon">✅</div>
|
|
<div class="conclusion-text">
|
|
<div class="label">Docker 아키텍처</div>
|
|
<div class="value">백엔드/프론트엔드 분리, 헬스 체크, Named Volume, 내부 네트워크 격리 적용</div>
|
|
</div>
|
|
</div>
|
|
<div class="conclusion-item">
|
|
<div class="conclusion-icon">✅</div>
|
|
<div class="conclusion-text">
|
|
<div class="label">사용자 경험</div>
|
|
<div class="value">한국어 UI, 다크 테마, 드래그&드롭, 실시간 진행률, 시리얼 모니터 제공</div>
|
|
</div>
|
|
</div>
|
|
<div class="conclusion-item">
|
|
<div class="conclusion-icon">⚠️</div>
|
|
<div class="conclusion-text">
|
|
<div class="label">브라우저 제한</div>
|
|
<div class="value">Web Serial API는 Chrome/Edge 89+만 지원 — Firefox, Safari 미지원</div>
|
|
</div>
|
|
</div>
|
|
<div class="conclusion-item">
|
|
<div class="conclusion-icon">⚠️</div>
|
|
<div class="conclusion-text">
|
|
<div class="label">인증 미구현</div>
|
|
<div class="value">현재 MVP 수준으로 인증 없이 누구나 업로드/삭제 가능</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>확장 로드맵</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>단계</th><th>기능</th><th>우선순위</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><strong>v1.1</strong></td>
|
|
<td>HTTPS 지원 (Nginx SSL/TLS), 업로드 인증 토큰</td>
|
|
<td><span class="badge badge-danger">높음</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>v1.2</strong></td>
|
|
<td>Rate Limiting, 플래시 이력 로깅, MAC 주소 기반 기기 인증</td>
|
|
<td><span class="badge badge-warn">중간</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>v2.0</strong></td>
|
|
<td>결제 연동 (Stripe), 1회용 플래시 토큰, 라이선스 서버</td>
|
|
<td><span class="badge badge-info">계획</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>v2.1</strong></td>
|
|
<td>S3 호환 스토리지 연동, CDN 배포, 다국어 UI</td>
|
|
<td><span class="badge badge-info">계획</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>v3.0</strong></td>
|
|
<td>OTA 업데이트 통합, 구독 모델, 관리자 대시보드</td>
|
|
<td><span class="badge badge-info">계획</span></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h3>상업화 가능성</h3>
|
|
<p>
|
|
Espressif 공식 esp-web-tools를 기반으로 하므로 기술 안정성이 높습니다.
|
|
ESP32-S3의 내장 USB-OTG 지원으로 별도 USB-UART 변환 칩 없이 플래싱이 가능하고,
|
|
이는 제품 원가 절감으로 이어집니다.
|
|
MAC 주소 기반 라이선스 바인딩(ESP32의 eFuse MAC은 위변조 난이도 높음)과
|
|
결합하면 펌웨어 유료 판매 플랫폼으로 확장할 수 있습니다.
|
|
</p>
|
|
<p>
|
|
현재 MVP 아키텍처(파일 기반 JSON 메타데이터)는 소규모 운영에 적합하며,
|
|
수백~수천 건 이상의 트래픽이 예상되면 PostgreSQL 등 관계형 DB와 S3 호환 스토리지로
|
|
마이그레이션이 권장됩니다.
|
|
</p>
|
|
</div>
|
|
|
|
<hr>
|
|
<p style="text-align:center;font-size:12px;color:var(--muted);padding-bottom:20px;">
|
|
ESP32 Web Flasher 기술 분석 보고서 — 2025년 5월 작성
|
|
</p>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|